If you've ever built a Telegram bot and tried to send a formatted message using parse_mode: "MarkdownV2", you've probably seen this error: "Can't parse entities: Character '.' is reserved and must be escaped." This happens because MarkdownV2 has 18 special characters that must be escaped with a backslash whenever they appear outside of formatting markers.
This guide covers every special character, when and how to escape them, the exceptions you need to know, and code examples in Python and Node.js.
In all places outside of code blocks, inline code, and link URLs, these characters must be preceded by a backslash:
| Character | Name | Escaped form | Common in |
|---|---|---|---|
_ | Underscore | \_ | Variable names |
* | Asterisk | \* | Math, emphasis |
[ | Left bracket | \[ | Arrays, links |
] | Right bracket | \] | Arrays, links |
( | Left paren | \( | Functions, URLs |
) | Right paren | \) | Functions, URLs |
~ | Tilde | \~ | Home paths |
` | Backtick | \` | Code snippets |
> | Greater than | \> | Quotes, arrows |
# | Hash | \# | Hashtags, IDs |
+ | Plus | \+ | Math, phone numbers |
- | Hyphen | \- | Dashes, subtraction |
= | Equals | \= | Assignment |
| | Pipe | \| | Tables, OR operator |
{ | Left brace | \{ | JSON, objects |
} | Right brace | \} | JSON, objects |
. | Period | \. | Every sentence |
! | Exclamation | \! | Emphasis, warnings |
Content wrapped in single backticks `like this` is treated as raw text. Only backticks and backslashes need escaping inside code spans. All other special characters are displayed as-is.
`print("Hello, World!")` → displays as: print("Hello, World!)
Same rule as inline code. Inside ```code blocks```, only backticks and backslashes need escaping. This is why showing JSON or code in a code block works without escaping every dot and brace.
In the URL part of [text](url) links, only ) and \ need escaping. The link text follows normal escape rules. The URL does not.
[Click here](https://example.com/path?q=test) → works as-is
`price: $19\.99` inside backticks, the backslash will be visible in the message. Don't escape inside code.
| Style | Syntax | Result |
|---|---|---|
| Bold | *bold text* | bold text |
| Italic | _italic text_ | italic text |
| Underline | __underline__ | underline |
| Strikethrough | ~strikethrough~ | |
| Inline code | `code` | code |
| Code block | ```code``` | (monospace block) |
| Link | [text](url) | clickable link |
| Spoiler | ||spoiler|| | (hidden text) |
import re
SPECIAL_CHARS = r'_*[]()~`>#+-=|{}.!'
def escape_mdv2(text: str) -> str:
"""Escape all MarkdownV2 special characters."""
return re.sub(r'([' + re.escape(SPECIAL_CHARS) + r'])', r'\\\1', text)
# Usage:
message = f"Price: {escape_mdv2('$19.99')} \\- *bold stays bold*"
bot.send_message(chat_id, message, parse_mode="MarkdownV2")
function escapeMdV2(text) {
return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
}
// Usage:
const price = escapeMdV2('$19.99');
const msg = `Price: ${price} \\- *bold stays bold*`;
bot.sendMessage(chatId, msg, { parse_mode: 'MarkdownV2' });
parse_mode: "HTML" instead of MarkdownV2. HTML only requires escaping <, >, and &, which is far simpler. MarkdownV2 is more powerful (supports spoilers, underline) but much more error-prone.
For most bots, HTML is the safer choice. It uses familiar tags like <b>, <i>, <code>, and only three characters need escaping. MarkdownV2 is better if you need spoiler text (||hidden||) or underline (__text__), which HTML doesn't support in Telegram.
Don't want to escape characters manually? Use our free Formatter tool — it auto-escapes all 18 characters while preserving formatting, links, and code blocks.
Try the Telegram Formatter Tool →