Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To convert plain text to HTML, first decide what “convert” means. If the text should appear literally, escape HTML-significant characters such as < and &, then place the result in a text element. If the source contains formatting conventions such as Markdown, parse that format into HTML. Escaping protects text from being interpreted as markup; it does not invent headings, paragraphs, lists, links, or line breaks.

Choose the right conversion path

The input format and the desired result determine the correct method. Treating every text string as if it were Markdown, or every conversion as simple escaping, produces incorrect output and can create security problems.

Goal Input Approach Result
Show the text exactly as entered Ordinary prose or user input Context-appropriate HTML output encoding Characters remain visible as text
Create a readable document Plain prose Choose paragraphs, headings, lists, and breaks explicitly Semantic HTML structure
Interpret lightweight markup Markdown Run a Markdown parser HTML elements generated from Markdown syntax
Insert text in an existing page Runtime string in a browser Use a safe DOM text sink such as textContent Text node, not parsed HTML

Display plain text literally

In an HTML text context, encode characters that have syntactic meaning. At minimum, a literal ampersand must become &amp;, a less-than sign must become &lt;, and a greater-than sign must become &gt;. Quotes also need encoding when they are placed in an attribute value.

Python standard-library example

Python’s html.escape() is suitable when the destination is an HTML text node. Its default quote=True also escapes single and double quotes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option
import html

plain_text = 'Use <tag> & "quotes"'
safe_text = html.escape(plain_text)
html_fragment = f'<p>{safe_text}</p>'

print(html_fragment)
# <p>Use &lt;tag&gt; &amp; &quot;quotes&quot;</p>

The resulting fragment renders the original characters instead of treating <tag> as an element. Escape once, at the point where the value is emitted. Keeping a canonical source string unescaped lets you encode it differently for a later output context.

Browser-side JavaScript

When inserting ordinary text into an element, assign it with textContent. The browser creates a text node and does not parse the value as HTML.

const output = document.querySelector('#output');
const plainText = 'Use <tag> & "quotes"';
output.textContent = plainText;

This advice applies to a text node only. It does not make a value safe for an attribute, URL, CSS value, event-handler attribute, or JavaScript string. Each context has different parsing rules and therefore requires its own encoding or safe API.

Turn prose into semantic HTML

Escaping answers “should these characters be interpreted as markup?” It does not answer “where are the paragraphs or headings?” For a document, create that structure deliberately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Paragraphs

Split the source into logical paragraphs and wrap each one in its own <p> element. Do not merely replace every newline with a paragraph tag without deciding how blank lines and intentional spacing should behave.

<p>First paragraph of the article.</p>
<p>Second paragraph with an escaped ampersand: A &amp; B.</p>

Headings and lists

Use a heading level that fits the page outline, then create lists from list items rather than relying on indentation or hyphens in a text blob.

<h2>Installation</h2>
<ol>
  <li>Download the package.</li>
  <li>Install its dependencies.</li>
  <li>Run the converter.</li>
</ol>

If the source is user-controlled, escape every inserted value before placing it inside these elements. The tags you author are structure; the data supplied by the user must remain data.

Preserve line breaks and whitespace

HTML collapses runs of ordinary whitespace in most elements. Choose a preservation strategy based on what a newline means in your content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use paragraphs for prose

When a blank line separates ideas, split the text into paragraphs. This produces better semantics and accessibility than adding many break tags.

Use <br> for meaningful single-line breaks

Poetry, addresses, or a chat transcript may require a break within one logical block. Escape the text first, then insert <br> only where the break belongs.

Use <pre> for preformatted material

Source code, logs, and fixed-width data can be wrapped in <pre><code>...</code></pre>. Escape the content inside; <pre> preserves whitespace but does not make untrusted markup safe.

Use CSS when the content is still one text block

For a text node that should retain newline characters, a class such as white-space: pre-wrap can preserve line breaks while allowing long lines to wrap. This is a presentation choice, not a conversion rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Convert Markdown to HTML

Use a Markdown parser only when the input is actually Markdown and its conventions should become elements. A parser can turn # Heading into a heading, - item into a list, and [label](url) into a link.

Python-Markdown example

import markdown

source = """# Release notes

- Faster startup
- Smaller downloads

Read the [guide](https://example.com/guide)."""

html_output = markdown.markdown(source)
print(html_output)

Python-Markdown’s convert(source) API returns HTML from a Markdown string. Parsing is not sanitization: Python-Markdown explicitly leaves responsibility for sanitizing generated HTML to the caller when input is untrusted. Apply a suitable HTML sanitizer before serving user-authored Markdown, and define which links, tags, and attributes are allowed.

Security rules for untrusted text

OWASP’s Cross Site Scripting Prevention Cheat Sheet describes the purpose of output encoding this way: “The purpose of output encoding (as it relates to XSS) is to convert untrusted input into a safe form where the input is displayed as data to the user without executing as code in the browser.”

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
  • Do not concatenate raw user input into an HTML string.
  • Encode for the destination context: HTML text, an HTML attribute, a URL, JavaScript, and CSS do not share one universal encoding function.
  • Prefer safe DOM APIs such as textContent for browser text insertion.
  • Do not treat entity escaping as a complete sanitizer. Sanitization is a separate allow-list operation for HTML that is permitted to remain active.
  • Do not double-escape. Applying escaping twice can display entity spellings such as &amp; instead of a single ampersand.
  • Keep the original, unescaped value as your source of truth when the same data may later be sent to another context.

Convert a text file to an HTML document

A complete document needs a doctype, language declaration, metadata, and a body. The example below treats each nonempty line as a paragraph; adapt the splitting rule if your file uses blank lines or another format.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path
import html

source = Path('notes.txt').read_text(encoding='utf-8')
paragraphs = [p.strip() for p in source.split('nn') if p.strip()]
body = 'n'.join(f'<p>{html.escape(p)}</p>' for p in paragraphs)

document = f'''<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Converted notes</title>
</head>
<body>
{body}
</body>
</html>'''

Path('notes.html').write_text(document, encoding='utf-8')

This script deliberately does not infer headings, links, or lists. Add those rules only when the source format defines them. If the file is Markdown, pass it through a Markdown parser instead of treating Markdown markers as ordinary prose.

Common mistakes and fixes

Symptom Cause Fix
The page shows literal tags such as &lt;p&gt; The value was escaped twice. Keep the source unescaped and encode once at the final HTML text output.
User input creates elements or runs script Raw input was concatenated into HTML or inserted with innerHTML. Use textContent for plain text, or context-appropriate encoding and sanitization for intended HTML.
Markdown markers remain visible Plain escaping was used even though the input was Markdown. Run a Markdown parser, then sanitize its output when the source is untrusted.
All lines run together Newlines were placed in normal HTML text, where whitespace collapses. Use paragraphs, intentional <br> elements, <pre>, or CSS white-space.
A quote breaks an attribute Text-node escaping was reused for an attribute without considering its context. Use an attribute-safe encoding or, preferably, set the property through a DOM API.
Links behave unexpectedly URL data was treated as ordinary text or accepted without validation. Encode for the URL/attribute context and apply your application’s URL validation policy.

Testing your converter

Test with characters that exercise the parser rather than only with alphabetic prose:

  • <tag> & "quotes"
  • Multiple consecutive spaces and tabs
  • Single, double, and blank lines
  • Markdown emphasis, lists, headings, and links if Markdown is supported
  • Strings containing apparent tags, event-handler text, and suspicious URLs
  • Empty input and very large input

Inspect the rendered page and the resulting DOM. Verify that intended structure exists, literal input remains literal, line breaks match the product requirement, and untrusted content cannot create active markup.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and maintenance considerations

For ordinary strings, escaping with a standard-library function or assigning textContent is inexpensive. Markdown parsing and sanitization perform more work because they tokenize and inspect the source; run them once per content update, cache trusted rendered output where appropriate, and avoid repeatedly converting the same string during rendering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep conversion rules close to the boundary where text enters HTML. That makes the trust decision visible, prevents accidental reuse of HTML-encoded data in another context, and allows a later output target to choose a different encoding strategy.

Or skip the browser setup

If your goal is to capture the HTML you have rendered as an image or PDF rather than build the conversion itself, ScreenshotNeo provides a website screenshot API. It accepts a URL and returns PNG, JPEG, WebP, or PDF; its clean-shot pipeline accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Failed loads, bot checks or CAPTCHAs, blank pages, timeouts, and cache hits are not billed, and each response identifies the page verdict and billing status in headers.

One GET request is enough (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Equivalent Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Equivalent Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes its features. The Free plan provides 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. When a rendered preview is all you need, sign up for ScreenshotNeo free.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Should I use an HTML template engine for plain text?

A template engine can place escaped values into a document, but it does not replace choosing the correct output context or deciding how paragraphs and line breaks should be represented.

Can I convert a .txt file to HTML without Python?

Yes. The essential steps are language-independent: read the file, split it according to its format, encode values for HTML, and emit the elements your document requires.

Is Markdown always safer than raw HTML?

No. A Markdown parser can generate HTML and may allow raw HTML, so untrusted Markdown still needs a sanitization policy before it is served.

Why does escaping not create links automatically?

Escaping only changes how special characters are interpreted. A link requires an explicit anchor element and a URL that has been handled for the attribute and URL contexts.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.