Python Keywords & Identifiers -- `list` Shadowing Crash
A variable named list triggered TypeError: 'list' object is not callable in production.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- Python keywords are reserved words with fixed meaning — you cannot use them as variable names
- Identifiers are names you create — they must start with a letter or underscore, contain only letters/digits/underscores
- Python 3.12 has exactly 35 keywords; check with
keyword.kwlist - The three capitalized keywords are True, False, None — all others are lowercase
- Overwriting a built-in like
listorprintis not a keyword violation but causes silent runtime bugs
Think of Python keywords as the words printed in bold on a board game rulebook — they have fixed meanings and you can't rename them or use them for anything else. Identifiers are like the names you write on your game pieces — you choose them yourself, but there are rules about what you're allowed to write. Just as you can't name your game piece 'Start' if that word already means something special on the board, you can't use a Python keyword as a variable name. Get those two ideas straight and the whole topic clicks.
Every language has a vocabulary. English has words like 'the', 'is', and 'if' that carry fixed grammatical meaning — you can't just decide 'if' now means a type of sandwich. Python works the same way. It has a built-in vocabulary of reserved words that power every program ever written in the language, and the moment you open a Python file, those rules are already in force whether you know about them or not. Understanding them isn't optional trivia — it's the foundation everything else is built on.
The problem most beginners hit is that Python's error messages when you misuse keywords are genuinely confusing at first. You try to name a variable 'list' or 'print' and something breaks in a way that doesn't obviously point to a naming problem. Knowing the rules up front means you spend your time actually building things rather than puzzling over cryptic SyntaxError messages at 11 pm.
By the end of this article you'll know every Python keyword by category, you'll be able to write identifiers that are both legal and professional, you'll understand exactly why Python enforces these rules, and you'll be able to spot and fix the three most common beginner naming mistakes on sight. Let's build this up from zero.
What Python Keywords Actually Are — and Why You Can't Touch Them
A Python keyword is a word that is permanently reserved by the language itself. Python's interpreter reads your code word by word, and when it hits a keyword, it doesn't ask what you meant — it already knows. Keywords are the grammar of Python. They're the skeleton that holds every statement together.
Python 3 currently has 35 keywords. Every single one of them serves a specific structural purpose. 'if' starts a condition. 'for' starts a loop. 'def' starts a function. 'True' and 'False' are the only two boolean values. 'None' represents the absence of a value. None of these can be reassigned, overwritten, or used as variable names.
You can see the complete list at any time by running two lines of Python. The 'keyword' module is part of Python's standard library — it ships with Python, no installation needed — and 'keyword.kwlist' gives you the full list in alphabetical order. This is worth memorising not because an interviewer will quiz you on all 35, but because recognising them on sight stops you accidentally walking into a naming collision.
Notice that all 35 keywords are lowercase except 'True', 'False', and 'None'. That capitalisation isn't a style choice — it's part of the specification. Python is case-sensitive, so 'true' (all lowercase) is not a keyword and can technically be used as a variable name, though doing so is a terrible idea for readability.
return as a variable name in a large codebase — but return is a keyword, so Python threw a SyntaxError at the assignment line.keyword.iskeyword() on any name that triggers an unexpected SyntaxError.Python Identifiers — The Naming Rules You Must Know Cold
An identifier is any name you create yourself — variable names, function names, class names, module names. You have freedom here, but Python enforces a firm set of rules. Break any one of them and you get a SyntaxError before your code even runs.
The rules are: (1) An identifier can only contain letters (a–z, A–Z), digits (0–9), and underscores (_). (2) It must not start with a digit — 'player1' is legal, '1player' is not. (3) It must not be a keyword. (4) It cannot contain spaces or special characters like @, $, %, !, or hyphens.
Python is case-sensitive. 'Score', 'score', and 'SCORE' are three completely different identifiers. This catches a lot of beginners who accidentally mix cases when referencing a variable they defined earlier.
Beyond the hard rules, the Python community follows PEP 8 — the official Python style guide. PEP 8 says: use lowercase_with_underscores for variables and functions ('player_score', not 'playerScore'), use CapitalisedWords for class names ('GamePlayer', not 'game_player'), and use ALL_CAPS_WITH_UNDERSCORES for constants ('MAX_SPEED = 300'). These aren't enforced by the interpreter, but every professional Python codebase follows them, and code reviewers will notice immediately if you don't.
Underscores carry special meaning too. A single leading underscore like '_internal_counter' signals to other developers that this is intended for internal use only. A double leading underscore like '__player_id' triggers Python's name mangling inside classes. And '__dunder__' names (double underscore on both sides) are Python's special method names like '__init__' and '__str__'.
player1 and playerl were both used — the second uses lowercase L instead of digit 1.score, Score, and SCORE are three different names.Keywords by Category — Understanding What Each Group Actually Does
Staring at 35 keywords in alphabetical order is overwhelming. Group them by what they do and the list becomes far more manageable — and far more meaningful.
The value keywords are the simplest: 'True', 'False', and 'None'. These are Python's only built-in literal constants. You'll use all three constantly.
The control-flow keywords — 'if', 'elif', 'else', 'for', 'while', 'break', 'continue', 'pass' — control the direction your code takes. Think of these as the signposts and junctions on a road.
The function and class keywords — 'def', 'return', 'lambda', 'class', 'yield' — are how you define reusable code blocks. 'lambda' creates a tiny one-liner function. 'yield' turns a function into a generator.
The error-handling keywords — 'try', 'except', 'finally', 'raise', 'assert' — are how Python deals with things going wrong. You'll reach for these the moment your programs start handling user input or file operations.
The import keywords — 'import', 'from', 'as' — bring external code into your file. 'as' lets you rename something on import, like 'import numpy as np'.
The scope keywords — 'global', 'nonlocal' — tell Python where a variable lives. The logical operators — 'and', 'or', 'not', 'in', 'is' — build conditions. And 'with', 'del', 'async', 'await' handle context management, deletion, and asynchronous programming respectively.
is to compare integers: if value is 1000: — it failed sometimes because Python caches small integers (-5 to 256) but not larger ones.== for value comparisons; reserve is for singleton checks (None, True, False).is vs == distinction is the most common keyword-related interview trap.The Silent Danger: Shadowing Built-in Names and Module Names
You now know that keywords like for and if are reserved — you can't use them as variable names. But there's a second, more dangerous category: built-in names like list, str, int, print, len, type, open, and input. These are NOT keywords. keyword.iskeyword('list') returns False. Python lets you assign to them without complaint.
That's the trap. The code runs. No SyntaxError. But somewhere else in your program, something that relied on list(...) or print(...) now breaks with a cryptic error.
Shadowing happens when you use a built-in name as your own identifier in a local scope. If you write list = [1, 2, 3] inside a function, then anywhere inside that function, calling will fail with list()TypeError: 'list' object is not callable. The built-in function is gone, replaced by your list.
The same applies to module names — don't name a file math.py or json.py, because when another module tries import math, Python finds your file first.
The fix is simple: use descriptive, specific names. Instead of list, use item_list or product_ids. Instead of str, use name_str or just name. Add a linting tool that flags shadowed built-in names: flake8-builtins or PyLint's builtin-attribute-shadowing rule.
flake8-builtins or PyLint's builtin-attribute-shadowing in your CI — they catch this before it ships.list in a helper function.list, str, print are NOT keywords — Python lets you shadow them.Special Identifiers: Underscore Conventions and Name Mangling
Beyond the hard rules, certain identifier patterns carry special meaning in Python. Understanding them is the difference between writing code that works and writing code that other experienced Python developers can read and maintain.
_single_leading_underscore: This is a convention, not enforced by Python. When you see _helper_function, it means 'this is internal to the module or class — don't use it from outside unless you know what you're doing'. The interpreter doesn't prevent access; it's a signal to other developers.
__double_leading_underscore: This triggers name mangling inside classes. Python renames the attribute to _ClassName__attribute. It exists to avoid naming conflicts in subclasses, not to implement private access. If you define __secret in a class, subclasses won't accidentally override it.
__dunder__ (double underscore both sides): These are Python's special method names, also called 'magic methods' or 'dunder methods'. You've seen __init__ (constructor), __str__ (string representation), __repr__, __len__, __eq__, and many more. Never invent your own dunder names — those are reserved by the language for future use.
Single underscore `_` as a name: Python programmers use _ as a throwaway variable name in loops or unpacking. For example, for _ in range(10): or _, y = point. It tells anyone reading your code 'I don't need this value'.
- _single: This is a convention. Other devs should respect it, but Python won't enforce it.
- __double: Name mangling prevents accidental overrides in subclasses — but it's not private.
- __dunder__: Never invent your own. These are the language's internal methods.
- _ as name: Use for loop variables you don't need — it improves readability.
__init__ as a method name (not the constructor) thinking double underscores made it 'private'._ClassName__init__, breaking all calls to it.Why `is` and `==` Will Burn You in Production — Identifier Identity vs Equality
You think you understand equality checks because you passed a coding quiz. Then you ship a memory-cache layer that breaks because you used is where you meant ==, or vice versa. These aren't interchangeable operators.
== tests value equality. is tests object identity — are both variables pointing to the exact same memory address? Python caches small integers and short strings, so a is b might pass in your test suite and fail in production when those objects fall out of the interning cache.
Always use == for comparing values. Use is only for singleton checks: if value is None, if flag is True. Never use is on strings, integers, or custom objects unless you explicitly control their lifecycle. The moment you compare two database records or config objects with is, you're gambling on CPython's internal optimisations you don't control.
This isn't theoretical. I've debugged cache invalidations that took three days because someone thought is was faster. It's not faster when it's wrong.
is for comparing strings from user input, API responses, or database reads. == is your friend. is is a loaded weapon.is only for singletons (None, True, False). Use == for everything else. Identity is not equality.Name Resolution: Why Python Looks Inside-Out and Your Import Breaks at 3 AM
Python resolves identifier names using the LEGB rule: Local, Enclosing, Global, Built-in. It sounds academic until your module imports a function that shadows a built-in, and your entire script silently starts failing.
When you write print = "hello", you just murdered the built-in print function in that scope. Any code after that line using dies with print()TypeError: 'str' object is not callable. I've seen this in production because someone used list as a variable name, then tried to call on a response payload.list()
Python doesn't warn you. It just walks the LEGB chain, finds your local variable first, and hands you the knife. To protect yourself, always name variables descriptively: user_list instead of list, input_data instead of input. Run import builtins; print(dir(builtins)) and commit that list to memory.
If you must use a shadowed built-in, access it explicitly: import builtins; builtins.print("still works"). But really, just don't shadow.
pylint --disable=all --enable=redefined-builtin on your codebase. It catches every shadowed built-in before your tests run.builtins.print() as an emergency escape hatch.Executable Identifiers: Why `eval()` and `exec()` Are a Security Nightmare
You built a config parser that reads user input and runs it through because it was "clean code." Congratulations, you just handed your server keys to anyone who can post eval()__import__('os').system('rm -rf /').
Python identifiers aren't just variable names — when used with or eval(), any valid identifier name becomes an attack vector. A user-controlled string containing exec(), globals()__import__, or os can execute arbitrary code. The worst part? It passes code review because the identifier os looks innocent.
Use for parsing safe Python literals — strings, numbers, tuples, lists, dicts, booleans, and None. That's it. If you need dynamic attribute access, use ast.literal_eval()getattr(obj, "attr_name", default) with a whitelist. Never use on user input. Not once. Not even "just in a test script."eval()
I've cleaned up a production breach where someone used to parse a JSON-like config. The attacker didn't even need to exploit it — a malformed payload crashed the entire service with a generic syntax error that took hours to debug because they'd hidden the eval in a helper function.eval()
eval() and exec() execute arbitrary Python code. Full stop. Use ast.literal_eval() for parsing literals, or write a proper parser with json, yaml.safe_load, or configparser.eval() and exec() like raw SQL injection — because that's exactly what they are. Parse user data with ast.literal_eval() or a purpose-built parser. Never trust an identifier name in user-controleed text.Soft Keywords: match, case, and _ in Python 3.10+
Python 3.10 introduced structural pattern matching with match and case as soft keywords. Unlike hard keywords (e.g., if, for), soft keywords are only reserved in specific contexts. For example, match can be used as a variable name outside a match statement, and case can be used similarly. The underscore _ is also special: in pattern matching, it acts as a wildcard that matches any value but does not bind it. This is a departure from its traditional use as a throwaway variable. Consider:
``python match command: case "quit": ``exit() case _: print("Unknown command")
Here _ matches anything not caught by previous patterns. However, using _ as a variable name elsewhere is still allowed but may confuse readers. Soft keywords reduce backward incompatibility while enabling new syntax. In production, be cautious: using match or case as identifiers in code that may run on Python <3.10 will cause syntax errors. Also, the wildcard _ in pattern matching does not bind a value, so you cannot reference it later. This differs from _ in loops or assignments. Understanding soft keywords helps you write modern Python without breaking legacy compatibility.
match or case as variable names if your codebase must support Python versions before 3.10, as it will cause syntax errors. Use _ carefully in pattern matching—it does not bind a value, so you cannot reference it later.match and case are reserved only in pattern matching contexts, enabling new syntax without breaking backward compatibility.Naming Conventions: snake_case, PascalCase, SCREAMING_SNAKE
Python's official style guide, PEP 8, recommends specific naming conventions for different code elements. These conventions are not enforced by the language but are widely adopted for readability and consistency.
- snake_case: Used for variable names, function names, and method names. All lowercase with underscores separating words. Example:
user_name,.calculate_total() - PascalCase (also known as UpperCamelCase): Used for class names. Each word starts with an uppercase letter, no underscores. Example:
CustomerAccount,DataProcessor. - SCREAMING_SNAKE: Used for constants. All uppercase with underscores. Example:
MAX_RETRIES,DEFAULT_TIMEOUT.
Additionally, single leading underscore _var indicates internal use (by convention), double leading underscore __var triggers name mangling in classes, and double leading and trailing underscore __var__ denotes special methods (dunder methods).
Following these conventions makes your code more predictable and easier for others to understand. For example:
```python # Good class UserProfile: def __init__(self, user_name): self.user_name = user_name
MAX_LOGIN_ATTEMPTS = 3 ```
In production, consistent naming reduces cognitive load and helps static analysis tools. Violating conventions (e.g., using PascalCase for functions) can confuse maintainers. While Python doesn't enforce these rules, adhering to them is a hallmark of professional code.
Reserved Keywords vs Built-in Names Confusion
A common source of confusion for Python beginners is the difference between reserved keywords and built-in names. Reserved keywords (e.g., if, for, while) are part of the language syntax and cannot be used as identifiers. Attempting to assign to a keyword raises a SyntaxError. Built-in names (e.g., list, dict, print) are functions or types that are always available but are not reserved. You can assign to them, but doing so shadows the original, potentially causing bugs.
For example:
# This is allowed but dangerous
list = [1, 2, 3]
print(list) # Works, but now list() is overwritten
After this, list no longer refers to the built-in list type. Calling list("abc") later would fail because list is now a list object, not a callable. This is a classic shadowing pitfall.
In contrast, you cannot do:
if = 5 # SyntaxError: invalid syntax
Keywords are syntactically protected; built-in names are not. To avoid confusion, never use built-in names as variable names. Use a linter to warn about shadowing. In production, shadowing a built-in can cause subtle bugs that are hard to trace, especially when the shadowed name is used elsewhere in the codebase.
Remember: keywords are reserved by the language parser; built-in names are just names in the built-in scope that can be overridden. Treat them with the same respect to avoid surprises.
pylint with --disable=W0622 for shadowing) to catch accidental shadowing of built-in names. In code reviews, flag any use of built-in names as identifiers.The Silent Crash: How Shadowing `list` Broke Production
TypeError: 'list' object is not callable on a line that called list(product_ids). The code had been working for months.list constructor.list as a variable name in a helper module: list = get_order_ids(). This overwrote the built-in list function in that module's namespace. When another module imported it (via from helper import *), the shadowed list leaked into the global namespace, breaking list(product_ids) two levels deep.list to order_list and add a linting rule that flags shadowed built-in names (flake8-plugin flake8-builtins does this). The import was changed to explicit imports to prevent namespace pollution.- Never use built-in names as variable names — they're not keywords, so Python won't stop you, but the runtime bugs are hard to trace.
- Explicit imports (
import helper) prevent shadowed names from leaking between modules. - Add
flake8-builtinsor PyLint'sbuiltin-attribute-shadowingcheck to your CI pipeline — it catches these before they reach production.
while True: fails)While with capital W is not a keyword. Use syntax highlighting — it will show keyword colour vs identifier colour.list, str, int, print, input. Run type(list) in the failing module — if it returns <class 'list'> incorrectly, someone shadowed it.python -c "import keyword; print(keyword.iskeyword('your_word'))"Check syntax highlighting in your editor — keywords are coloured differently.| File | Command / Code | Purpose |
|---|---|---|
| explore_keywords.py | all_keywords = keyword.kwlist | What Python Keywords Actually Are |
| identifier_rules_demo.py | player_name = "Alice" # lowercase_with_underscores — PEP 8 for variable... | Python Identifiers |
| keywords_by_category.py | game_active = True # True — one of only two boolean values | Keywords by Category |
| shadowing_builtins.py | def process_orders(): | The Silent Danger |
| underscore_conventions.py | class Player: | Special Identifiers |
| IdentitySnare.py | def validate_user_session(cached_obj, fresh_obj): | Why `is` and `==` Will Burn You in Production |
| LegbShadow.py | def crash_reporting(): | Name Resolution |
| EvalNightmare.py | user_input = "__import__('os').system('whoami')" # Attacker payload | Executable Identifiers |
| soft_keywords.py | match = 42 # Valid: match is a soft keyword | Soft Keywords |
| naming_conventions.py | user_name = "alice" | Naming Conventions |
| keywords_vs_builtins.py | list = [1, 2, 3] # Shadows built-in list | Reserved Keywords vs Built-in Names Confusion |
Key takeaways
_single means 'internal use', __double triggers name mangling, __dunder__ is for Python's special methods only.Interview Questions on This Topic
Can you name five Python keywords and explain what each one does?
if: starts a conditional block; executes code only if the condition is True.
- for: iterates over a sequence (list, tuple, string, etc.) or any iterable.
- def: defines a function.
- return: exits a function and optionally sends a value back to the caller.
- None: the sole value of NoneType, representing the absence of a value.
Interviewers expect you to pick keywords you actually use daily, not recite a memorised list. Demonstrating that you understand the context (e.g., mentioning that None is a singleton checked with is None) adds depth.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Python Basics. Mark it forged?
9 min read · try the examples if you haven't