Code generators, form validators, refactoring tools, and systems that transform external names into Python identifiers need to know whether a word can be used as a variable, attribute, function, or class. The Python keyword module exposes the reserved words recognized by the current interpreter and the soft keywords that have special meaning only in selected grammatical contexts.
This guide covers iskeyword(), kwlist, issoftkeyword(), and softkwlist, including version-aware identifier validation. It complements our articles about tokenize, symtable, codeop, Python AST, and types.
What a reserved word is
Reserved words participate in Python’s grammar and cannot be ordinary identifiers.
class = "report" # SyntaxError
return = 10 # SyntaxErrorFamiliar examples include class, def, return, if, for, while, try, import, and lambda. The list depends on the Python version and should not be hard-coded in long-lived tooling.
Check with iskeyword()
keyword.iskeyword() reports whether a string is a reserved word for the current interpreter.
import keyword
print(keyword.iskeyword("class")) # True
print(keyword.iskeyword("customer")) # FalseThe function checks reserved-word status only. It does not validate every identifier rule.
Inspect kwlist
keyword.kwlist contains reserved words in alphabetical order.
for word in keyword.kwlist:
print(word)The list supports educational interfaces, syntax highlighting, tests, and alternative-name generation.
Do not mutate kwlist
Although the exposed object is a list, changing it does not alter Python’s grammar and can break code that relies on the module.
# Do not do this
keyword.kwlist.append("customer")Treat it as read-only information. Copy it or build a frozenset for application use.
RESERVED = frozenset(keyword.kwlist)What soft keywords are
Soft keywords receive special meaning only in certain grammatical contexts. Outside those contexts, they may remain valid identifiers.
This mechanism lets Python evolve while reducing breakage of existing programs. Modern features such as pattern matching and type declarations use contextual words.
Check soft keywords
print(keyword.issoftkeyword("match"))
print(keyword.issoftkeyword("case"))
print(keyword.issoftkeyword("customer"))The official keyword documentation recommends querying the module rather than maintaining fixed lists because grammar evolves.
Inspect softkwlist
for word in keyword.softkwlist:
print(word)Not every soft keyword must be rejected as a name. The decision depends on where the identifier will appear.
A soft keyword may still be an identifier
match = "text"
case = 10
print(match, case)In structural pattern matching, the same words participate in syntax.
match value:
case 0:
print("zero")A code generator must consider the complete context instead of blocking every soft keyword unconditionally.
Validate a complete identifier
Combine str.isidentifier() with iskeyword().
def valid_identifier(name: str) -> bool:
return name.isidentifier() and not keyword.iskeyword(name)
print(valid_identifier("customer_id"))
print(valid_identifier("2customers"))
print(valid_identifier("class"))isidentifier() follows the Unicode identifier rules of the current release. It does not determine whether a name is readable or allowed by project policy.
Unicode names
Python accepts many Unicode characters in identifiers.
print("ação".isidentifier())
print("π".isidentifier())The official lexical analysis reference describes normalization and permitted categories. Public APIs may prefer ASCII for interoperability, but that is a project policy rather than a language requirement.
Generate a safe identifier
import re
import unicodedata
def make_identifier(text: str) -> str:
text = unicodedata.normalize("NFKC", text).strip()
text = re.sub(r"\W+", "_", text, flags=re.UNICODE)
text = text.strip("_") or "value"
if text[0].isdigit():
text = "_" + text
if keyword.iskeyword(text):
text += "_"
if not text.isidentifier():
raise ValueError("could not create identifier")
return textDo not use normalization alone for authorization decisions. Visually similar Unicode characters can still differ.
The trailing-underscore convention
class_ = "Report"
from_ = "source"
async_ = FalseA trailing underscore is the conventional escape for reserved words. Keep a mapping between original external field names and Python identifiers.
Attribute names
Reserved words can be dictionary keys and can even be stored as dynamic attributes.
obj.__dict__["class"] = "special"
print(getattr(obj, "class"))However, obj.class is invalid syntax. Code generators must reject or transform such names even when setattr() accepts them.
JSON and database fields
External schemas may legitimately contain class, from, or global. Do not silently alter the external schema.
FIELD_MAP = {
"class": "class_",
"from": "from_",
}
value = payload["class"]
model.class_ = valueSerialization back to the external format should restore the original name.
Generate function parameters
def safe_signature(names):
for name in names:
if not valid_identifier(name):
raise ValueError(f"invalid parameter: {name!r}")
return ", ".join(names)Even with valid identifiers, concatenating and executing code remains dangerous when other parts come from users. Prefer closures, data structures, or restricted AST construction.
Keywords in templates
A template that generates classes, dataclasses, or APIs should validate class names, attributes, parameters, and methods separately.
A name accepted by getattr() is not necessarily valid after a dot in source code. Compile representative output in tests.
Compatibility across Python versions
A word may become reserved or contextual in a future release. Tools producing code for another runtime should not rely only on the interpreter running the generator.
Options include:
- validating with the target interpreter;
- maintaining versioned tables from official sources;
- compiling generated code under every supported release;
- using containers or a CI Python matrix.
Do not reuse one version’s list forever
Persisting an old kwlist indefinitely may permit syntax that is invalid in newer releases or reject words that changed status.
Record the interpreter version alongside generated artifacts and test minimum and maximum supported versions.
Syntax highlighting
Editors can use kwlist for hard keywords and softkwlist for contextual highlighting. Coloring every soft keyword everywhere creates false positives.
Accurate highlighting requires a tokenizer or parser. The keyword module provides classification, not grammatical position.
Integration with tokenize
tokenize commonly reports both identifiers and keywords as NAME tokens. The parser makes the final distinction.
import io
import tokenize
source = b"if value: pass\n"
for token in tokenize.tokenize(io.BytesIO(source).readline):
if token.type == tokenize.NAME:
print(token.string, keyword.iskeyword(token.string))This supports lightweight analyzers, while soft keywords still need context.
Integration with AST
The parser applies the actual grammar of the current version. ast.parse() or compilation is the strongest confirmation that a complete source fragment is syntactically valid.
import ast
try:
ast.parse("customer = 1")
except SyntaxError as error:
print(error)Parsing validates syntax without executing the source.
Refactoring tools
When renaming symbols, check:
- whether the new name is an identifier;
- whether it is a hard keyword;
- whether it conflicts in the scope;
- whether a soft keyword is valid in that context;
- whether strings and configuration references also need updates.
symtable and AST help analyze scopes and references.
API validation
def validate_field(name):
if not name.isidentifier():
raise ValueError("field is not a Python identifier")
if keyword.iskeyword(name):
raise ValueError(f"{name!r} is reserved")An API can offer an alternative automatically, but should clearly expose the mapping.
Performance
Module checks are fast. Tools validating millions of values can build immutable sets.
KEYWORDS = frozenset(keyword.kwlist)
SOFT_KEYWORDS = frozenset(keyword.softkwlist)Rebuild these sets whenever the interpreter changes.
Testing
def test_identifiers():
assert valid_identifier("customer")
assert valid_identifier("ação")
assert not valid_identifier("class")
assert not valid_identifier("2customers")
assert not valid_identifier("customer-id")Include every current keyword, contextual words in several positions, Unicode, empty strings, and names close to built-ins.
Built-ins are not keywords
Names such as list, dict, str, id, and input are not reserved.
print(keyword.iskeyword("list")) # FalseAssigning to them is legal but shadows useful built-ins.
list = [1, 2, 3]
# list("abc") now failsLinters can enforce this additional policy.
Common mistakes
- Validating only with
iskeyword(). - Rejecting every soft keyword in every context.
- Maintaining a stale manual list.
- Assuming
setattr()guarantees valid dot syntax. - Confusing built-ins with reserved words.
- Generating for another version without testing it.
- Mutating
kwlistorsoftkwlist. - Executing code merely to validate syntax.
Best practices
- Combine
isidentifier()andiskeyword(). - Evaluate soft keywords in context.
- Query or test the actual target runtime.
- Keep mappings for transformed external names.
- Use AST or compilation for complete fragments.
- Test all supported Python versions.
- Lint shadowed built-ins.
- Handle Unicode normalization deliberately.
Conclusion
The Python keyword module provides the official hard and contextual keyword lists for the running interpreter. iskeyword() and issoftkeyword() help validators, generators, editors, and refactoring tools follow language evolution.
Correct validation goes beyond one list: names must satisfy isidentifier(), soft keywords depend on context, and source intended for another version must be tested there. These layers let tools produce readable identifiers without generating invalid syntax.





