Beginner
Most Python developers know f-strings and str.format() for string interpolation. But Python’s standard library also ships a string module with three tools that solve problems f-strings cannot: safe templating for user-facing text where untrusted users supply the template, a clean set of character constants for validation and generation tasks, and a Formatter class for building custom format mini-languages. These tools fill specific gaps that show up in real production code, and knowing they exist saves you from reinventing them.
The string module is part of the Python standard library — no installation required. It is a compact module with no heavy dependencies, and every class in it can be imported and used in two or three lines. The module is not something you reach for every day, but when the need arises it solves the problem in exactly the right way.
In this article you will learn all three components of the string module in depth. We will start with string.Template and its safe substitution method, then walk through the character constant strings, build a password generator with them, implement a custom Formatter subclass, and finish with a real-life example: a notification template engine that assembles user-facing messages safely. Every section includes runnable code and expected output.
string Module Quick Example
Here is the three-line version — template substitution, a character constant, and one constant-driven function all at once:
# quick_string_module.py
import string
# Template substitution -- safer than f-strings for user-supplied templates
tmpl = string.Template("Hello $name, your order #$order_id is ready.")
print(tmpl.substitute(name="Alice", order_id=7842))
# Check that a token uses only URL-safe characters
safe_chars = string.ascii_letters + string.digits + "-_"
token = "user-abc_123"
print("Token valid:", all(c in safe_chars for c in token))
Output:
Hello Alice, your order #7842 is ready.
Token valid: True
Two useful operations in six lines, no imports beyond the standard library. The sections below unpack each component with more realistic examples and edge cases.
What Is the string Module?
The string module is a small but focused standard library module with three main components:
| Component | What it provides | When to use it |
|---|---|---|
| string.Template | $-based substitution with safe_substitute() | User-supplied templates, config files, untrusted input |
| Character constants | ascii_letters, digits, punctuation, etc. | Validation, generation, char-class membership tests |
| string.Formatter | Subclassable custom format engine | Domain-specific format mini-languages |
Think of it as a toolkit for string operations that are not quite “manipulation” (that is str methods) and not quite “parsing” (that is re or textwrap). The module fills the gap between raw string operations and format strings when you need safe, controlled substitution or well-defined character sets.
string.Template for Safe Substitution
F-strings and str.format() execute Python expressions: f"{user_input}" will happily call methods or access attributes on any object if the template comes from user input. string.Template supports only simple variable substitution — no method calls, no attribute access, no expressions. That restriction is exactly what makes it safe for user-supplied templates:
# template_demo.py
import string
# Basic substitution with $variable syntax
tmpl = string.Template("Hi $name! You have $count new messages.")
result = tmpl.substitute(name="Bob", count=3)
print(result)
# safe_substitute() leaves missing variables intact instead of raising KeyError
partial = string.Template("Dear $title $surname, your invoice for $$amount is due.")
# Only supply title -- surname is missing, amount uses $$ (literal dollar sign)
print(partial.safe_substitute(title="Dr"))
# ${variable} syntax for disambiguation
ambiguous = string.Template("${user}name is not the same as $username")
print(ambiguous.substitute(user="super", username="admin"))
Output:
Hi Bob! You have 3 new messages.
Dear Dr $surname, your invoice for $amount is due.
superman is not the same as admin
The key distinction is substitute() vs safe_substitute(). The former raises KeyError if a placeholder is missing from the mapping; the latter leaves missing placeholders in the output as literal text. Use substitute() when you control the template and know all variables will be present. Use safe_substitute() when the template comes from a config file or user input and you want to fill in what you can without crashing. The $$ escape sequence produces a literal dollar sign in the output.
Customising the Template Delimiter
You can subclass string.Template to change the delimiter from $ to anything that suits your domain — useful for templates that already contain dollar signs (SQL, shell scripts, Markdown pricing tables):
# custom_template.py
import string
class BraceTemplate(string.Template):
"""Use {{variable}} syntax instead of $variable."""
delimiter = "{{"
pattern = r"""
\{\{(?:
(?P<escaped>\{\{) | # Escape sequence: {{{{
(?P<named>[_a-z][_a-z0-9]*)\}\} | # {{varname}}
(?P<braced>[_a-z][_a-z0-9]*)\}\} |
(?P<invalid>)
)
"""
# Simpler subclass: just change the delimiter character
class HashTemplate(string.Template):
delimiter = "#"
tmpl = HashTemplate("Hello #name, total: #amount")
print(tmpl.substitute(name="Alice", amount="$99.95"))
Output:
Hello Alice, total: $99.95
The delimiter class attribute is the single character (or string) that signals the start of a placeholder. By switching to #, dollar signs in the template are now ordinary text. This pattern comes up when you are generating shell scripts (where $VAR is used by the shell itself), SQL queries, or LaTeX documents where every dollar sign has a different meaning.
Character Constants
The string module exposes a set of pre-built character strings that cover all the common character classes:
# constants_demo.py
import string
print("ascii_letters:", string.ascii_letters[:20], "...")
print("ascii_lowercase:", string.ascii_lowercase)
print("ascii_uppercase:", string.ascii_uppercase)
print("digits:", string.digits)
print("hexdigits:", string.hexdigits)
print("octdigits:", string.octdigits)
print("punctuation:", string.punctuation)
print("whitespace repr:", repr(string.whitespace))
print("printable (first 30):", string.printable[:30])
Output:
ascii_letters: abcdefghijklmnopqrst ...
ascii_lowercase: abcdefghijklmnopqrstuvwxyz
ascii_uppercase: ABCDEFGHIJKLMNOPQRSTUVWXYZ
digits: 0123456789
hexdigits: 0123456789abcdefABCDEF
octdigits: 01234567
punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
whitespace repr: ' \t\n\r\x0b\x0c'
printable (first 30): 0123456789abcdefghijklmnopqrst
These are plain strings — you can iterate over them, test membership with in, use them in re.escape(), or pass them directly to random.choices(). They save you from hard-coding character ranges and from subtle bugs like forgetting that hexdigits includes uppercase letters or that octdigits stops at 7.
Password Generator with Constants
The character constants become genuinely useful in generation and validation tasks. Here is a cryptographically strong password generator that uses them:
# password_gen.py
import string
import secrets # cryptographically secure RNG
def generate_password(
length: int = 16,
use_upper: bool = True,
use_digits: bool = True,
use_symbols: bool = True,
) -> str:
"""
Generate a cryptographically secure password.
Guarantees at least one character from each enabled character class.
"""
charset = string.ascii_lowercase
required = [secrets.choice(string.ascii_lowercase)]
if use_upper:
charset += string.ascii_uppercase
required.append(secrets.choice(string.ascii_uppercase))
if use_digits:
charset += string.digits
required.append(secrets.choice(string.digits))
if use_symbols:
# Use a safer subset -- exclude visually ambiguous chars
symbols = "!@#$%^&*()-_=+"
charset += symbols
required.append(secrets.choice(symbols))
# Fill remaining length with random chars from full charset
remaining = length - len(required)
password_chars = required + [secrets.choice(charset) for _ in range(remaining)]
# Shuffle to avoid predictable positions for required chars
secrets.SystemRandom().shuffle(password_chars)
return "".join(password_chars)
def validate_password(password: str, min_length: int = 12) -> tuple[bool, list[str]]:
"""Return (is_valid, list_of_failures)."""
failures = []
if len(password) < min_length:
failures.append(f"Too short: {len(password)} < {min_length}")
if not any(c in string.ascii_uppercase for c in password):
failures.append("Missing uppercase letter")
if not any(c in string.digits for c in password):
failures.append("Missing digit")
if not any(c in string.punctuation for c in password):
failures.append("Missing special character")
return len(failures) == 0, failures
# Generate and validate three passwords
for _ in range(3):
pwd = generate_password(length=16)
valid, issues = validate_password(pwd)
print(f"{pwd} | valid={valid}")
Output (example -- passwords are random):
k!R8mP2@qXzV#nLw | valid=True
$4uHjW*eNbYv!9Td | valid=True
mQ3@rKpZ#2XsVtEa | valid=True
The validator uses string.ascii_uppercase, string.digits, and string.punctuation directly in membership tests instead of hard-coding character ranges. If you ever need to adjust which characters count as "special", you change the constant or define your own subset -- the logic stays the same. Using secrets instead of random ensures the generator is cryptographically suitable for security-sensitive passwords.
string.Formatter for Custom Format Mini-Languages
string.Formatter is the engine behind str.format() exposed as a class you can subclass. Override format_field() to add custom conversions to your format strings:
# custom_formatter.py
import string
class ReportFormatter(string.Formatter):
"""
Extends str.format() with custom format codes:
:curr -- format as currency e.g. 1234.5 -> "$1,234.50"
:pct -- format as percentage e.g. 0.857 -> "85.7%"
:yn -- bool to Yes/No e.g. True -> "Yes"
"""
def format_field(self, value, format_spec):
if format_spec == "curr":
return f"${value:,.2f}"
elif format_spec == "pct":
return f"{value * 100:.1f}%"
elif format_spec == "yn":
return "Yes" if value else "No"
# Fall back to standard formatting for anything else
return super().format_field(value, format_spec)
fmt = ReportFormatter()
report_template = (
"Customer: {name}\n"
"Total spent: {total:curr}\n"
"Conversion rate: {conv_rate:pct}\n"
"Active subscriber: {is_subscriber:yn}\n"
"Plan: {plan:>10}" # standard right-align still works
)
print(fmt.format(
report_template,
name="Alice Chen",
total=1234.5,
conv_rate=0.857,
is_subscriber=True,
plan="Pro"
))
Output:
Customer: Alice Chen
Total spent: $1,234.50
Conversion rate: 85.7%
Active subscriber: Yes
Plan: Pro
Custom format codes coexist with all standard format specs -- :>10 right-align still works alongside your :curr and :pct codes. This pattern is useful for report generation, invoice templates, and any domain where you want a consistent formatting vocabulary that non-developers can use without knowing Python format syntax.
Real-Life Example: Notification Template Engine
Here is a complete notification engine that loads message templates from a config dict, fills them safely with string.Template.safe_substitute(), and validates that all expected placeholders were filled:
# notification_engine.py
import string
from dataclasses import dataclass
from typing import Any
# Message templates -- in production, load from a YAML/JSON file
TEMPLATES = {
"order_confirmed": string.Template(
"Hi $first_name, your order #$order_id has been confirmed. "
"Estimated delivery: $delivery_date."
),
"password_reset": string.Template(
"Hi $first_name, use this link to reset your password: $reset_link "
"(expires in $expiry_minutes minutes)."
),
"subscription_renewal": string.Template(
"Hi $first_name, your $plan_name subscription renews on $renewal_date "
"for $$renewal_amount."
),
}
@dataclass
class Notification:
template_name: str
context: dict[str, Any]
channel: str = "email" # "email", "sms", "push"
def render(self) -> str:
tmpl = TEMPLATES.get(self.template_name)
if tmpl is None:
raise ValueError(f"Unknown template: {self.template_name}")
# Use safe_substitute so partial context does not crash
rendered = tmpl.safe_substitute(self.context)
# Check for any unresolved $placeholders
remaining = [w for w in rendered.split() if w.startswith("$")]
if remaining:
print(f" Warning: unresolved placeholders: {remaining}")
return rendered
# Send a batch of notifications
notifications = [
Notification(
"order_confirmed",
{"first_name": "Alice", "order_id": "ORD-7842", "delivery_date": "May 2"},
),
Notification(
"password_reset",
{"first_name": "Bob", "reset_link": "https://app.example.com/reset/abc123",
"expiry_minutes": 30},
channel="sms",
),
Notification(
"subscription_renewal",
{"first_name": "Carol", "plan_name": "Pro", "renewal_date": "May 15"},
# renewal_amount deliberately missing to show safe_substitute in action
),
]
for notif in notifications:
message = notif.render()
print(f"[{notif.channel.upper()}] {message}")
print()
Output:
[EMAIL] Hi Alice, your order #ORD-7842 has been confirmed. Estimated delivery: May 2.
[SMS] Hi Bob, use this link to reset your password: https://app.example.com/reset/abc123 (expires in 30 minutes).
Warning: unresolved placeholders: ['$renewal_amount.']
[EMAIL] Hi Carol, your Pro subscription renews on May 15 for $renewal_amount.
The engine uses safe_substitute() so a missing value never crashes production. The warning check catches missing placeholders before they reach users in a way that does not fail silently. Because the templates live in a dict (loaded from config in production), non-developers can edit message copy without touching Python code -- the template syntax is simple enough for a product manager to understand.
Frequently Asked Questions
When should I use string.Template instead of f-strings?
Use string.Template when the template itself comes from outside your codebase -- config files, database records, user input, or CMS content. F-strings are evaluated at write time in your source code; string.Template is evaluated at runtime against a provided mapping. If you let users type f"Hello {os.system('rm -rf /')}" into a template field, an f-string would execute it. string.Template only performs simple key substitution -- no expressions, no attribute access, no function calls.
Should I use string constants or regex for character class checks?
For simple membership tests (does this string contain only digits?), string constants are faster and more readable: all(c in string.digits for c in s). For pattern matching (does this string match a specific digit pattern?), use re. For large strings, str.translate() with a translation table built from string.digits can be significantly faster than a generator expression because it is implemented in C.
What can Formatter do that str.format() cannot?
str.format() is actually implemented using string.Formatter internally. Subclassing lets you add custom format specs (like :curr above), override how keys are looked up in the mapping (useful for case-insensitive keys or computed properties), implement format validation before output, and add logging or auditing around format calls. You can also override vformat() to control the entire rendering pipeline.
What is string.capwords()?
string.capwords(s) splits on whitespace, capitalises each word, and rejoins with single spaces. It differs from str.title() in that it handles apostrophes correctly: "it's" becomes "It's" with capwords, but "It'S" with title() (which capitalises after any non-letter, including apostrophes). Use capwords for human names and titles; use title() only when you want the technical definition of title case.
Can I use string.Template with nested or chained templates?
string.Template does not support nested substitution natively -- the result of a substitution is a plain string and is not re-processed for more $ placeholders. If you need two-pass templating (a template whose substituted values contain further $ placeholders), call substitute() twice: once with the outer context, then again on the result with the inner context. This is deliberate -- one-pass substitution prevents infinite loops from recursive templates.
Conclusion
The string module solves three specific problems cleanly: Template gives you safe substitution for user-supplied templates, the character constants give you pre-built character sets for validation and generation, and Formatter lets you build custom format mini-languages without monkey-patching the str type. None of these replace f-strings for everyday use -- they fill the gaps where f-strings are either unsafe or inflexible.
Take the notification engine from the real-life example and extend it by loading the TEMPLATES dict from a YAML file with yaml.safe_load(). Your product team can now edit notification copy in YAML without touching Python, and the engine validates that every required placeholder is filled before sending. Read the full string module documentation at https://docs.python.org/3/library/string.html.