typing.LiteralString represents strings considered literal or derived only from other literal strings. It helps sensitive APIs distinguish developer-defined text from dynamic content received through users, files, networks, configuration, or databases. This distinction can reduce injection risks in SQL, shell commands, templates, and format strings, but it does not replace parameterization, escaping, validation, authorization, or least privilege.
This guide explains inference, string operations, SQL parameters, dynamic identifiers, shells, templates, builders, casts, taint-analysis limits, stubs, compatibility, testing, and layered security.
The dynamic-string problem
def execute_sql(query: str) -> None:
...
name = input("Name: ")
execute_sql(f"SELECT * FROM users WHERE name = '{name}'")The signature accepts any string. A checker cannot distinguish a fixed query from one constructed with external input. The runtime example is vulnerable to injection.
Declaring LiteralString
from typing import LiteralString
def execute_sql(query: LiteralString) -> None:
...
execute_sql("SELECT * FROM users")A string written directly in source is compatible. A general variable typed as str normally is not.
External input should be rejected
query: str = input("SQL: ")
execute_sql(query) # expected static errorThe checker reports that an arbitrary string does not satisfy the trusted contract. This creates a useful review and CI barrier.
LiteralString is a subtype of str
A LiteralString can be passed anywhere a normal str is accepted. The reverse is not true. Display, logging, and ordinary text processing continue to work.
Concatenating literals
prefix: LiteralString = "SELECT id, name "
suffix: LiteralString = "FROM users"
query = prefix + suffix
execute_sql(query)Operations that combine only LiteralStrings can preserve trusted status. Exact inference depends on the checker and expression.
f-strings
table: LiteralString = "users"
query = f"SELECT * FROM {table}"An f-string may remain LiteralString when every interpolated expression is trusted. If any expression is only str, trust should be lost.
Do not interpolate SQL data
Even with LiteralString, structure and values should remain separate:
def query(sql: LiteralString, parameters: tuple[object, ...]) -> None:
...
name = input("Name: ")
query(
"SELECT * FROM users WHERE name = ?",
(name,),
)The query is literal and external data is sent separately. The database driver handles encoding and escaping. Parameterization is the primary defense.
Dynamic table names
Drivers usually parameterize values, not SQL identifiers. Select dynamic identifiers from a closed set:
from typing import Literal
Table = Literal["users", "orders"]
def table_name(table: Table) -> LiteralString:
if table == "users":
return "users"
return "orders"Do not cast arbitrary input to LiteralString. Validate against allowlists and return known literals.
Shell commands
def run_command(command: LiteralString) -> None:
...The type may discourage commands built from external input, but the safer design is avoiding shell=True and passing an argument list:
subprocess.run(["git", "show", revision], check=True)LiteralString does not make shell parsing safe.
HTML templates
def render(template: LiteralString, context: dict[str, object]) -> str:
...A renderer can require a developer-controlled template and accept dynamic data separately. The engine must still apply context-aware escaping for HTML text, attributes, JavaScript, CSS, and URLs.
Logging format strings
def log(format_string: LiteralString, *args: object) -> None:
...
log("user %s signed in", name)Keeping the format literal helps logging tools and prevents externally controlled placeholders. Values remain separate arguments.
Internationalization
Translated text loaded from catalogs is a normal str, even if the catalog belongs to the project. Do not force it into LiteralString merely to satisfy an API. Translation systems need a different trust model.
Configuration files
Text read from YAML, JSON, or environment variables is dynamic. It does not become literal because the file is committed to a repository. Deployment systems and operators may change configuration.
Trust-preserving functions
def add_limit(query: LiteralString) -> LiteralString:
return query + " LIMIT 100"A function can accept and return LiteralString when it adds only literal content. If it incorporates an arbitrary str, the return type should become str.
Safe builders
def order_by_name(query: LiteralString) -> LiteralString:
return query + " ORDER BY name"For dynamic choices, map enums or Literal values to fixed fragments. Avoid free-form fragments.
cast does not sanitize
from typing import cast
user_input = input("SQL: ")
query = cast(LiteralString, user_input)The cast only silences the checker. It does not validate, escape, or change the string. Casting untrusted input destroys the guarantee.
Validation does not automatically create LiteralString
After a regex or allowlist check, a checker may still see str. Prefer functions that choose and return explicit known literals or a closed Literal union. Avoid a general “trust this string” helper.
Literal versus LiteralString
Literal["asc", "desc"] describes exact allowed values, and those values are compatible with LiteralString. Literal is ideal for closed options. LiteralString is useful when an API accepts any text built exclusively from trusted literals.
TypeGuard cannot prove origin
A TypeGuard cannot generally determine that an arbitrary runtime string originated from source literals. Origin is a static flow property, not observable content. Two equal strings may have different trust histories.
External libraries
A library return annotated as str loses trusted status even if the implementation currently returns constants. A library may promise LiteralString when it genuinely preserves literal origin across all supported behavior.
Stub files
Database, logging, and template libraries may use LiteralString in .pyi files. Test consumer experience with mypy and pyright. An annotation that is too restrictive can block legitimate use cases.
Not full taint tracking
LiteralString is a simple approximation. It does not track sources, sanitizers, contexts, encodings, database drivers, or cross-process flows like a complete taint-analysis system.
Layered security
Use LiteralString as an additional layer. SQL parameters, argument arrays, contextual escaping, allowlists, input validation, authorization, minimal privileges, and security tests remain necessary.
Compatibility
Use typing_extensions.LiteralString on older versions. Precision also depends on the checker. Keep tooling current and add typing cases to CI.
Common mistakes
- Casting external input: no sanitization occurs.
- Interpolating SQL values: use driver parameters.
- Assuming shell commands are safe: avoid shell=True.
- Treating configuration as literal: it is dynamic data.
- Confusing it with full taint analysis: the model is limited.
- Relying only on types: runtime defenses remain essential.
Complete example: SQL repository
from typing import Literal, LiteralString
Order = Literal["name", "created_at"]
def order_column(order: Order) -> LiteralString:
if order == "name":
return "name"
return "created_at"
def list_users(connection, term: str, order: Order):
column = order_column(order)
query: LiteralString
if column == "name":
query = (
"SELECT id, name FROM users "
"WHERE name LIKE ? ORDER BY name"
)
else:
query = (
"SELECT id, name FROM users "
"WHERE name LIKE ? ORDER BY created_at"
)
cursor = connection.execute(query, (f"%{term}%",))
return cursor.fetchall()Search values are parameterized. The ordering column is selected through a closed Literal union, and each final query is a source literal. External input never becomes SQL structure.
Static tests
from typing import assert_type
assert_type(order_column("name"), LiteralString)
text: str = input()
# execute_sql(text) should fail static analysisInclude accepted and rejected cases to protect the boundary when stubs and checkers change.
When to use it
Use LiteralString in APIs where textual structure should be developer-defined: queries, templates, formats, and expressions. Do not add it to ordinary text-processing functions that intentionally accept arbitrary user content.
Conclusion
LiteralString declares that an API expects text originating from trusted literals. It helps static tools reject dynamic strings in sensitive positions and encourages separation between structure and data.
The official Python LiteralString documentation explains inference. Use it as defense in depth, never as a replacement for parameterized queries, escaping, allowlists, validation, authorization, and least privilege.







