Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    21 Original Xbox Games Just Landed on PC, and KOTOR Finally Has Achievements

    November 13, 2026

    Amazfit Active 2 Review: Is It Still Worth $99 in 2026?

    November 13, 2026

    Apple Counts 100,000 iPhone Duo Apps. Venmo, DoorDash and Google Docs Still Show Black Bars.

    November 13, 2026
    Facebook
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • Gaming
    • Smartwatch
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Mark Strings as Safe in Django Templates
    Blog

    How to Mark Strings as Safe in Django Templates

    Ethan CaldwellBy Ethan CaldwellSeptember 4, 20268 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    HTML code displayed on a computer monitor
    Photo via Pexels
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    In Django you mark a string as safe with django.utils.safestring.mark_safe(), or in a template with the safe filter. Both tell the template engine to skip HTML escaping for that value. In almost every real case you should reach for django.utils.html.format_html() instead, because it escapes your arguments for you and only marks the finished fragment as safe. Django’s own documentation says as much: for building up fragments of HTML, you should normally be using format_html().

    Quick answer: Use format_html("<span class='badge'>{}</span>", label) in views, model methods and admin display methods. Use mark_safe() only on a string you built entirely yourself with no variable data in it. In templates use {{ value|safe }} only when the value came from a trusted source, never for anything a user typed.

    The reason this matters is that Django’s autoescaping is the main thing standing between a comment field and a cross site scripting hole. Turning it off is a decision, not a formatting convenience, and the API is designed so that the safe and the dangerous approaches look almost identical in a diff. Knowing which of the four mechanisms you are using, and why, is most of the job.

    What Django escapes by default

    Every variable rendered in a Django template is passed through the escaper unless something has marked it safe. Five characters are replaced.

    CharacterBecomes
    <&lt;
    >&gt;
    &&amp;
    Double quote&quot;
    Single quote&#x27;

    So a comment body containing a script tag renders as visible text rather than executing. That default is what you are overriding whenever you mark something safe, and you are overriding it for the whole value, not for the parts you meant to allow.

    The four mechanisms, and when each applies

    MechanismEscapes your data?Use it when
    format_html()Yes, every argumentAlways, when building HTML in Python
    format_html_join()Yes, plus the separatorRepeating one fragment over an iterable
    mark_safe()No, nothingA literal you wrote with no interpolation at all
    {{ value|safe }}No, nothingThe value is already trusted HTML from your own code

    Why format_html is the right default

    Its contract is precise. The first argument, the format string, is not escaped. Every other positional and keyword argument goes through conditional_escape() before it reaches str.format(), and the finished result has mark_safe() applied. That is exactly the split you want: your markup is trusted because you typed it, your data is escaped because you did not.

    from django.utils.html import format_html
    from django.utils.safestring import mark_safe
    
    # Dangerous. label is interpolated unescaped.
    def badge_bad(label):
        return mark_safe(f'<span class="badge">{label}</span>')
    
    # Correct. The markup is literal, label is escaped.
    def badge_good(label):
        return format_html('<span class="badge">{}</span>', label)
    
    badge_good('<script>alert(1)</script>')
    # '<span class="badge">&lt;script&gt;alert(1)&lt;/script&gt;</span>'

    The two functions are three characters apart in a code review and worlds apart in behavior. That is why teams that care about this ban bare mark_safe() with an f string or a percent format in a linter rule rather than relying on reviewers to spot it.

    Recommended for you:

    Best State to Start an LLC: Alabama or Mississippi?
    Blog·Sep 4, 2026

    Best State to Start an LLC: Alabama or Mississippi?

    For a list, format_html_join() saves you a loop. It takes a separator, one format string, and a generator of argument tuples or mappings, and it escapes the separator too.

    from django.utils.html import format_html_join
    
    def user_items(users):
        return format_html_join(
            "\n",
            "<li>{} {}</li>",
            ((u.first_name, u.last_name) for u in users),
        )
    
    def book_items(books):
        return format_html_join(
            "\n",
            '<li data-id="{id}">{id} {title}</li>',
            ({"id": b.id, "title": b.title} for b in books),
        )
    Warning: Number formatting and the other advanced str.format() options do not work in format_html(), because every argument is passed through conditional_escape() first. Format the number in Python, then pass the finished string.

    mark_safe, and how safety gets lost

    mark_safe() returns a SafeString, which is a subclass of str that the template engine recognizes as needing no further escaping. It can be called more than once on the same string and it can be used as a decorator. What it never does is inspect the content.

    The subtle failure is that safety does not survive string operations. Any method that returns a new object hands you back a plain str, and the mark is gone.

    >>> from django.utils.safestring import mark_safe
    >>> s = mark_safe("<b>Hello World</b>   ")
    >>> type(s)
    <class 'django.utils.safestring.SafeString'>
    >>> type(s.strip())      # whitespace removed, safety removed with it
    <class 'str'>

    So a helper that marks a fragment safe and then hands it to something that trims, joins, or truncates it will silently start escaping again. That is the benign version. The dangerous version is the reverse: a value that was escaped, then passed through a function that reassembles it and marks the result safe.

    The worst pattern of all involves strip_tags(). Django’s documentation is emphatic that no guarantee is provided about the result being HTML safe, and that you must never mark safe the result of a strip_tags() call without escaping it first.

    from django.utils.html import escape, strip_tags
    from django.utils.safestring import mark_safe
    
    mark_safe(strip_tags(user_input))   # never do this
    escape(strip_tags(user_input))      # this is fine; escape applies mark_safe itself

    In templates: safe, safeseq and autoescape

    The template side has three tools. The safe filter marks one value as needing no escaping. The safeseq filter applies safe to each element of a sequence, which is what you need before something like join, because safe on a list would stringify the whole list first. The autoescape block turns escaping on or off for a region.

    {{ page.rendered_body|safe }}
    
    {{ tag_links|safeseq|join:", " }}
    
    {% autoescape off %}
      {{ email_template_fragment }}
    {% endautoescape %}

    Filter order is load bearing. Applying a filter after safe can make the value unsafe again, so {{ var|safe|escape }} ends up escaped. Treat {% autoescape off %} as a red flag in review: it covers everything inside the block including variables added to that block six months later by someone who did not read this far.

    Admin and model methods

    Admin columns are where most teams meet this API for the first time, because a plain string returned from a display method is escaped and shows up as literal angle brackets in the changelist.

    from django.contrib import admin
    from django.utils.html import format_html
    
    @admin.register(Article)
    class ArticleAdmin(admin.ModelAdmin):
        list_display = ["title", "status_badge"]
    
        @admin.display(description="Status")
        def status_badge(self, obj):
            color = "green" if obj.published else "gray"
            return format_html(
                '<span style="color: {}">{}</span>',
                color,
                obj.get_status_display(),
            )

    Same rule as everywhere else. The markup is a literal in the format string, the two variables are arguments, and the return value is already a SafeString so the admin renders it as markup.

    Troubleshooting

    Angle brackets show up as text. The value is not marked safe. If you built it in Python, switch to format_html(). If it arrives from elsewhere and you trust it, use the safe filter at the point of rendering.

    Output is double escaped, showing &amp;lt;. Something escaped an already escaped string. Use conditional_escape() rather than escape() in helpers that might receive either kind, since it leaves preescaped strings alone.

    A helper works in one template and escapes in another. Look for a string operation between the two call sites. Slicing, stripping or concatenating a SafeString with a plain str produces a plain str.

    A custom filter escapes its output. Register it with @register.filter(is_safe=True) only if it genuinely cannot introduce unsafe characters, and use conditional_escape() on any input you interpolate.

    Everything inside a block renders raw. There is an {% autoescape off %} above it, possibly in an included template or a parent block. Grep the whole template tree, not just the file you are looking at.

    Frequently asked questions

    Is mark_safe ever the right call?

    Yes, for a string literal you wrote with no interpolation, such as a fixed icon fragment or a constant separator. The moment any variable enters the string, switch to format_html(). If you cannot point at every character in the output and say where it came from, do not mark it safe.

    What is the difference between escape and conditional_escape?

    Recommended for you:

    Marketing Is Changing Fast: What the Consultancies Say
    Blog·Sep 4, 2026

    Marketing Is Changing Fast: What the Consultancies Say

    Both encode ampersands, quotes and angle brackets and return a safe string. The difference is that conditional_escape() leaves already escaped strings alone, so it will not double escape. Use it inside helpers that may be handed either raw text or a SafeString.

    Can I allow some HTML from users, like bold and links?

    Not with these tools. mark_safe() is all or nothing and strip_tags() makes no safety guarantee. Allowing a subset of tags requires a dedicated sanitizer library that parses the markup and rebuilds it from an allowlist, then you render its output.

    Does the safe filter work when autoescape is off?

    It has no effect there. With autoescaping disabled for a block, nothing is being escaped in the first place, so marking a value safe changes nothing. That asymmetry is one more reason to leave autoescaping on and mark individual values instead.

    Why does my number lose its formatting inside format_html?

    Because arguments pass through conditional_escape(), which coerces them to strings before str.format() sees them, so format specifiers on those arguments do not apply. Build the formatted string first and pass the result in as a plain argument.

    The bottom line

    Default to format_html() and format_html_join() for anything you assemble in Python, and treat a bare mark_safe() around an f string as a bug rather than a style choice. That single habit removes the entire category of mistake, and it reads no worse than the dangerous version.

    Everything else follows from knowing that SafeString is just a subclass of str with a flag, and that the flag disappears the moment anything creates a new string. Check the django.utils.html reference and the template builtins on autoescaping when you are unsure. If you work across template languages, our guide to unpacking multiple variables in a Jinja2 loop covers the same terrain in the other engine, and the escaping discipline generalizes to running database queries safely and to careful string comparison.

    About this article: GeekBlog covers U.S. technology news, AI, phones, smartwatches and gaming. Every story is written and checked under our Editorial Policy. Spotted a mistake or have a story tip? Contact our editors.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleHow to Get Digital Marketing Clients and Projects
    Next Article Best State to Start an LLC: Alabama or Mississippi?
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem: iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      10 Mins Read

      Google Ads Keyword Planner: How It Shows the Most Relevant Keywords

      11 Mins Read

      How to Use Remarketing Techniques for Better Conversions

      11 Mins Read

      How to Conduct A/B Testing for Marketing Campaigns

      10 Mins Read

      AMP for WP Plugin Vulnerability: What Was Fixed and What to Do

      11 Mins Read

      How to Create a Facebook Business Page in 2026

      11 Mins Read

      How to Convert GMT Time to Other Time Zones in C++

      Top Posts

      Every iPhone Camera Ranked in 2026 (Best to Worst)

      July 6, 2026370 Views

      Best Browser for Android in 2026: Top Picks Ranked

      July 7, 2026293 Views

      iPhone Battery Replacement Cost: Every Model, Apple vs Repair Shop

      October 6, 2026194 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      MapQuest Now Shows the Plate Readers on Your Route. It Won’t Say Where the Map Comes From.

      October 19, 2026652 Views

      Apple Told Its Stores Not to Expect iPhone Duo Walk-Ins. Carriers Can Beat January by Weeks.

      October 20, 2026303 Views

      Apple Watch Black Friday 2026: Early Deals, Prices to Beat and When to Buy

      November 8, 2026118 Views
      Our Picks

      21 Original Xbox Games Just Landed on PC, and KOTOR Finally Has Achievements

      November 13, 2026

      Amazfit Active 2 Review: Is It Still Worth $99 in 2026?

      November 13, 2026

      Apple Counts 100,000 iPhone Duo Apps. Venmo, DoorDash and Google Docs Still Show Black Bars.

      November 13, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      HEICJPG.online - Convert HEIC to JPG online
      Facebook
      • About GeekBlog: Our Mission, Team and Editorial Standards
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      • Editorial Policy
      • Cookie Policy
      • Your Privacy Choices
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.