Two days of regex — you've earned a quieter lesson. The string module is one of Python's smallest standard library modules, but it has three things you reach for constantly without knowing they're in a module: text constants, template strings, and the Formatter class. Let's start with constants.
Text constants — like, the alphabet? I've been writing "abcdefghijklmnopqrstuvwxyz" when I needed to check if something was alphabetic. Please tell me there's a better way.
There is a much better way:
import string
print(string.ascii_lowercase) # 'abcdefghijklmnopqrstuvwxyz'
print(string.ascii_uppercase) # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
print(string.ascii_letters) # both, combined
print(string.digits) # '0123456789'
print(string.punctuation) # '!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~'
print(string.whitespace) # space, tab, newline, etc.
print(string.printable) # all printable ASCIIstring.digits is "0123456789". That's a plain string. So I can do char in string.digits instead of char.isdigit(). And string.punctuation for stripping non-alphanumeric characters from log messages before analysis.
These are just strings — you can use in, join them, slice them, put them in sets for fast membership testing. For log analysis, set(string.ascii_letters + string.digits + "-_") is a fast membership set for valid identifier characters in service names.
What about string.Template? I've seen it mentioned but always reached for f-strings instead.
f-strings are the right choice for most formatting. string.Template is specifically useful when the template comes from outside your code — a config file, a user-provided format string, or an email template stored in a database. The substitution syntax is $variable or ${variable}:
import string
# Template from a config file or database
alert_template = string.Template(
"ALERT: $level in $service at $timestamp — $message"
)
result = alert_template.substitute(
level="ERROR",
service="auth",
timestamp="2026-04-07T09:14:33Z",
message="Token expired for user maya.patel"
)
print(result)
# ALERT: ERROR in auth at 2026-04-07T09:14:33Z — Token expired for user maya.patelsubstitute() versus safe_substitute() — what's the difference?
substitute() raises KeyError if a variable in the template has no matching key in the dict you pass. safe_substitute() leaves unmatched $variables as-is. For templates you control, substitute() — a missing variable is a bug you want to catch. For templates from users or config files where the template might reference variables you don't have yet, safe_substitute() is safer:
import string
template = string.Template("Service: $service — Status: $status — Notes: $notes")
partial = {"service": "auth", "status": "ERROR"}
# safe_substitute leaves $notes unchanged
print(template.safe_substitute(partial))
# Service: auth — Status: ERROR — Notes: $notesThe ops team sends alert templates for different severity levels — the body is stored in their monitoring config. I'd use string.Template to fill in the log data. If their template references a field my log entry doesn't have, safe_substitute() leaves the placeholder so I can see what's missing instead of crashing.
Exactly the right use case. And textwrap — in the textwrap module, not string — is worth knowing alongside it:
import textwrap
long_message = "Token validation failed because the JWT secret has been rotated and the client is still using the old signing key from before the rotation window closed"
print(textwrap.fill(long_message, width=60))
print(textwrap.shorten(long_message, width=80, placeholder="..."))textwrap.fill() wraps to a width, textwrap.shorten() truncates with a placeholder. For the ops team's email reports — long log messages get truncated to 80 characters in the summary view but the full text is available on click. That's a real formatting need.
And now you know the standard library tool for it. The string and textwrap modules are quieter than re — they don't get you from "30 lines of string splitting" to "two lines." But they cover the formatting edge cases that make a report look professional versus amateurish.
string.Template for external templates, textwrap for word wrapping. And I can delete the line in my report generator where I type out "abcdefghijklmnopqrstuvwxyz" to check if a service name is alphabetic. Today was a good day.
Tomorrow we look at difflib — Python's built-in sequence comparison module. You've been asking the ops team to manually identify what changed between two log snapshots. difflib automates that. It's the quietest module in Week 2 and the one that will save you the most time next week when the ops lead asks "what changed between yesterday's log and today's?".
The string module occupies an unusual position in the standard library: it predates most Python best practices and contains a mix of utilities that range from essential to obsolete. The modern essentials are the character constants and string.Template. The rest is historical.
string.ascii_lowercase, string.ascii_uppercase, string.ascii_letters, string.digits, string.punctuation, string.whitespace, and string.printable are plain Python strings. Their value is not in the type they return but in the fact that they're already defined, correctly, in a canonical place. char in string.digits is equivalent to char.isdigit() for ASCII digits (but not for Unicode digit characters — str.isdigit() handles those). For membership testing in hot loops, set(string.ascii_letters + string.digits) pre-builds a hash set for O(1) lookup.
f-strings are the right choice for formatting you control — the template is in your code, the variables are known at the call site. string.Template is for formatting you don't control — when the template string comes from a config file, a database, user input, or an external system. The $variable syntax is simpler and less powerful than f-string expressions, which is a feature: Template substitution cannot execute arbitrary code. This matters when template strings come from untrusted sources.
Template.substitute() raises KeyError for missing keys. Template.safe_substitute() leaves unmatched placeholders as literal $variable strings. The choice depends on trust: substitute when you control the template and missing keys are programming errors; safe_substitute when the template comes from outside and may reference fields you don't have.
The textwrap module — separate from string but a natural companion — provides textwrap.fill(text, width) for word-wrapped output and textwrap.shorten(text, width, placeholder="...") for truncation. For reports that display log messages in a fixed-width column, shorten is the standard tool. textwrap.dedent() removes common leading whitespace from multi-line strings — useful for cleaning heredoc strings in tests and configuration.
Sign up to write and run code in this lesson.
Two days of regex — you've earned a quieter lesson. The string module is one of Python's smallest standard library modules, but it has three things you reach for constantly without knowing they're in a module: text constants, template strings, and the Formatter class. Let's start with constants.
Text constants — like, the alphabet? I've been writing "abcdefghijklmnopqrstuvwxyz" when I needed to check if something was alphabetic. Please tell me there's a better way.
There is a much better way:
import string
print(string.ascii_lowercase) # 'abcdefghijklmnopqrstuvwxyz'
print(string.ascii_uppercase) # 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
print(string.ascii_letters) # both, combined
print(string.digits) # '0123456789'
print(string.punctuation) # '!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~'
print(string.whitespace) # space, tab, newline, etc.
print(string.printable) # all printable ASCIIstring.digits is "0123456789". That's a plain string. So I can do char in string.digits instead of char.isdigit(). And string.punctuation for stripping non-alphanumeric characters from log messages before analysis.
These are just strings — you can use in, join them, slice them, put them in sets for fast membership testing. For log analysis, set(string.ascii_letters + string.digits + "-_") is a fast membership set for valid identifier characters in service names.
What about string.Template? I've seen it mentioned but always reached for f-strings instead.
f-strings are the right choice for most formatting. string.Template is specifically useful when the template comes from outside your code — a config file, a user-provided format string, or an email template stored in a database. The substitution syntax is $variable or ${variable}:
import string
# Template from a config file or database
alert_template = string.Template(
"ALERT: $level in $service at $timestamp — $message"
)
result = alert_template.substitute(
level="ERROR",
service="auth",
timestamp="2026-04-07T09:14:33Z",
message="Token expired for user maya.patel"
)
print(result)
# ALERT: ERROR in auth at 2026-04-07T09:14:33Z — Token expired for user maya.patelsubstitute() versus safe_substitute() — what's the difference?
substitute() raises KeyError if a variable in the template has no matching key in the dict you pass. safe_substitute() leaves unmatched $variables as-is. For templates you control, substitute() — a missing variable is a bug you want to catch. For templates from users or config files where the template might reference variables you don't have yet, safe_substitute() is safer:
import string
template = string.Template("Service: $service — Status: $status — Notes: $notes")
partial = {"service": "auth", "status": "ERROR"}
# safe_substitute leaves $notes unchanged
print(template.safe_substitute(partial))
# Service: auth — Status: ERROR — Notes: $notesThe ops team sends alert templates for different severity levels — the body is stored in their monitoring config. I'd use string.Template to fill in the log data. If their template references a field my log entry doesn't have, safe_substitute() leaves the placeholder so I can see what's missing instead of crashing.
Exactly the right use case. And textwrap — in the textwrap module, not string — is worth knowing alongside it:
import textwrap
long_message = "Token validation failed because the JWT secret has been rotated and the client is still using the old signing key from before the rotation window closed"
print(textwrap.fill(long_message, width=60))
print(textwrap.shorten(long_message, width=80, placeholder="..."))textwrap.fill() wraps to a width, textwrap.shorten() truncates with a placeholder. For the ops team's email reports — long log messages get truncated to 80 characters in the summary view but the full text is available on click. That's a real formatting need.
And now you know the standard library tool for it. The string and textwrap modules are quieter than re — they don't get you from "30 lines of string splitting" to "two lines." But they cover the formatting edge cases that make a report look professional versus amateurish.
string.Template for external templates, textwrap for word wrapping. And I can delete the line in my report generator where I type out "abcdefghijklmnopqrstuvwxyz" to check if a service name is alphabetic. Today was a good day.
Tomorrow we look at difflib — Python's built-in sequence comparison module. You've been asking the ops team to manually identify what changed between two log snapshots. difflib automates that. It's the quietest module in Week 2 and the one that will save you the most time next week when the ops lead asks "what changed between yesterday's log and today's?".
The string module occupies an unusual position in the standard library: it predates most Python best practices and contains a mix of utilities that range from essential to obsolete. The modern essentials are the character constants and string.Template. The rest is historical.
string.ascii_lowercase, string.ascii_uppercase, string.ascii_letters, string.digits, string.punctuation, string.whitespace, and string.printable are plain Python strings. Their value is not in the type they return but in the fact that they're already defined, correctly, in a canonical place. char in string.digits is equivalent to char.isdigit() for ASCII digits (but not for Unicode digit characters — str.isdigit() handles those). For membership testing in hot loops, set(string.ascii_letters + string.digits) pre-builds a hash set for O(1) lookup.
f-strings are the right choice for formatting you control — the template is in your code, the variables are known at the call site. string.Template is for formatting you don't control — when the template string comes from a config file, a database, user input, or an external system. The $variable syntax is simpler and less powerful than f-string expressions, which is a feature: Template substitution cannot execute arbitrary code. This matters when template strings come from untrusted sources.
Template.substitute() raises KeyError for missing keys. Template.safe_substitute() leaves unmatched placeholders as literal $variable strings. The choice depends on trust: substitute when you control the template and missing keys are programming errors; safe_substitute when the template comes from outside and may reference fields you don't have.
The textwrap module — separate from string but a natural companion — provides textwrap.fill(text, width) for word-wrapped output and textwrap.shorten(text, width, placeholder="...") for truncation. For reports that display log messages in a fixed-width column, shorten is the standard tool. textwrap.dedent() removes common leading whitespace from multi-line strings — useful for cleaning heredoc strings in tests and configuration.