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.
- 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 ast.literal_eval() for parsing safe Python literals — strings, numbers, tuples, lists, dicts, booleans, and None. That's it. If you need dynamic attribute access, use getattr(obj, "attr_name", default) with a whitelist. Never use eval() on user input. Not once. Not even "just in a test script."
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.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.Key takeaways
_single means 'internal use', __double triggers name mangling, __dunder__ is for Python's special methods only.Common mistakes to avoid
4 patternsUsing a built-in name like 'list', 'input', 'print', or 'str' as a variable name
Using hyphens in variable names instead of underscores
Assuming Python keywords are case-insensitive
Using lowercase 'true', 'false', or 'none' as variable names
True (uppercase) in the same scope, you'll have two different values causing logical errors.True, False, None are the only valid forms.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?
8 min read · try the examples if you haven't