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
Python keywords are reserved words that form the language's syntax — you cannot use them as variable names, function names, or any other identifiers. There are 35 keywords in Python 3.11+ (e.g., if, for, class, yield), and attempting to assign to one raises a SyntaxError at parse time.
Identifiers are the names you give to variables, functions, classes, and modules; they must follow strict rules: start with a letter or underscore, followed by letters, digits, or underscores, and are case-sensitive. The real-world trap is not keywords themselves (the interpreter catches those) but shadowing built-in names like list, dict, str, or id — which are identifiers, not keywords.
When you write list = [1,2,3], you silently overwrite the built-in list type, causing downstream code that expects the constructor to fail with TypeError: 'list' object is not callable. This is a common rookie mistake that can crash production pipelines, especially in data-heavy codebases using pandas or NumPy where list is frequently used as a variable name.
The article also covers underscore conventions (_, __, __name__), name mangling in classes, and the critical difference between is (identity comparison, checks ) and id()== (equality comparison, checks __eq__), which burns teams when comparing singletons like None or sentinel objects.
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.
import keyword # Python's built-in keyword module — no install needed # Get the full list of all reserved keywords in your Python version all_keywords = keyword.kwlist print(f"Python {__import__('sys').version.split()[0]} has {len(all_keywords)} keywords:\n") # Print keywords in rows of 5 so they're easy to read for index, word in enumerate(all_keywords): # end='' prevents a newline after each word; we add our own spacing print(f"{word:<12}", end='') # After every 5th keyword, drop to a new line if (index + 1) % 5 == 0: print() # moves the cursor to the next line print() # final newline for clean terminal output # Check whether a specific word is a keyword — returns True or False word_to_check = "while" print(f"\nIs '{word_to_check}' a Python keyword? {keyword.iskeyword(word_to_check)}") word_to_check = "speed" print(f"Is '{word_to_check}' a Python keyword? {keyword.iskeyword(word_to_check)}")
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__'.
# ── LEGAL IDENTIFIERS ───────────────────────────────────────────────────────── player_name = "Alice" # lowercase_with_underscores — PEP 8 for variables player_score = 0 # digits are fine anywhere except the first position MAX_LEVEL = 100 # ALL_CAPS signals a constant — never meant to change _checkpoint_data = [1, 5, 9] # single leading underscore = internal/private use print(f"Player: {player_name}") print(f"Score: {player_score}") print(f"Max level: {MAX_LEVEL}") print(f"Checkpoints: {_checkpoint_data}") # ── CASE SENSITIVITY IN ACTION ──────────────────────────────────────────────── total_coins = 50 # variable 'total_coins' — lowercase Total_Coins = 200 # COMPLETELY different variable — Python sees a new name entirely print(f"\ntotal_coins = {total_coins}") # prints 50 print(f"Total_Coins = {Total_Coins}") # prints 200 — different variable! # ── PEP 8 CLASS NAMING ──────────────────────────────────────────────────────── class GamePlayer: # CapitalisedWords (PascalCase) for class names def __init__(self, name): # __init__ is a dunder — Python's constructor method self.name = name # 'self.name' stores the player's name on the object new_player = GamePlayer("Bob") # create an instance of GamePlayer print(f"\nNew player name: {new_player.name}") # ── WHAT HAPPENS WITH ILLEGAL NAMES — commented out to allow file to run ────── # 1player = "invalid" # SyntaxError: starts with a digit # player-name = "invalid" # SyntaxError: hyphens are not allowed # for = 10 # SyntaxError: 'for' is a keyword
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.
# This single program intentionally demonstrates keywords from every category. # Read each section's comment to see which keyword group is in play. # ── VALUE KEYWORDS: True, False, None ──────────────────────────────────────── game_active = True # True — one of only two boolean values high_score_set = False # False — the other boolean value last_winner = None # None — represents 'no value yet' print("=== Game State ===") print(f"Game active: {game_active}") print(f"High score set: {high_score_set}") print(f"Last winner: {last_winner}") # ── CONTROL FLOW KEYWORDS: if, elif, else, for, while, break, continue, pass ── player_lives = 3 current_level = 7 print("\n=== Difficulty Check ===") if current_level >= 10: # 'if' starts a condition block print("Expert territory!") elif current_level >= 5: # 'elif' = else-if — checked only if 'if' was False print("Mid-game — stay sharp.") # this branch runs: level is 7 else: # 'else' catches everything else print("Just getting started.") print("\n=== Inventory Scan ===") inventory = ["sword", "shield", "potion", "torch", "map"] for item in inventory: # 'for' loops over each item in the list if item == "potion": # skip the potion — keep it for emergencies continue # 'continue' jumps straight to the next iteration if item == "torch": break # 'break' exits the loop entirely when torch found print(f" Equipped: {item}") print("\n=== Lives Countdown ===") while player_lives > 0: # 'while' keeps looping as long as condition is True print(f" Lives remaining: {player_lives}") player_lives -= 1 # subtract 1 each iteration to avoid infinite loop # ── FUNCTION KEYWORDS: def, return, lambda ──────────────────────────────────── print("\n=== Scoring Functions ===") def calculate_bonus(base_score, multiplier): # 'def' defines a reusable function """Returns the final score after applying the bonus multiplier.""" bonus_score = base_score * multiplier return bonus_score # 'return' sends the result back to caller double_points = lambda score: score * 2 # 'lambda' — a one-liner function final_score = calculate_bonus(150, 3) print(f"Bonus score: {final_score}") print(f"Doubled score: {double_points(final_score)}") # ── ERROR-HANDLING KEYWORDS: try, except, finally, raise ────────────────────── print("\n=== Safe Division ===") def safe_divide(total_points, number_of_players): try: # 'try' — attempt this risky operation result = total_points / number_of_players except ZeroDivisionError: # 'except' — caught! handle the error print(" Can't divide by zero players — game not started yet.") return None finally: # 'finally' — runs whether error or not print(" Division attempt complete.") return result print(safe_divide(900, 3)) # normal case print(safe_divide(900, 0)) # triggers ZeroDivisionError # ── LOGICAL / MEMBERSHIP KEYWORDS: and, or, not, in, is ────────────────────── print("\n=== Win Condition ===") has_key = True boss_defeated = True if has_key and boss_defeated: # 'and' — both must be True print("You win! Both conditions met.") print(f"'map' in inventory: {'map' in inventory}") # 'in' checks membership print(f"last_winner is None: {last_winner is None}") # 'is' checks identity
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.
# ── THE PROBLEM: Shadowing a built-in ─────────────────────────────────────── # This runs fine — no SyntaxError, no warning def process_orders(): list = [101, 102, 103] # 'list' is now a variable, not the constructor! print(f"Processing {len(list)} orders") # Later in the same function... new_orders = list([201, 202]) # TypeError: 'list' object is not callable # Because 'list' now refers to the list object above, not the built-in function # ── THE FIX ─────────────────────────────────────────────────────────────────── def process_orders_fixed(): order_ids = [101, 102, 103] # descriptive name, no shadowing print(f"Processing {len(order_ids)} orders") new_order_ids = list([201, 202]) # works fine — list() is still the built-in # ── HOW TO CHECK FOR SHADOWED NAMES ─────────────────────────────────────────── import builtins def check_shadowed_names(): # Get all built-in names builtin_names = dir(builtins) # Get names in the current local scope local_names = dir() # Find any that shadow built-ins shadowed = [name for name in local_names if name in builtin_names] if shadowed: print(f"WARNING: You're shadowing these built-in names: {shadowed}") else: print("No shadowing detected.")
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 LEADING UNDERSCORE: Internal use convention ────────────────────── class Player: def __init__(self, name): self.name = name self._hidden_power = 100 # internal — not part of the public API def attack(self): return self._hidden_power * 0.5 p = Player("Alice") print(p._hidden_power) # Works fine — but you're violating the convention # ── DOUBLE LEADING UNDERSCORE: Name mangling ────────────────────────────────── class Character: def __init__(self, name, power): self.name = name self.__power = power # name-mangled to _Character__power def get_power(self): return self.__power class SuperCharacter(Character): def __init__(self, name, power, bonus): super().__init__(name, power) self.__power = power + bonus # This is a DIFFERENT attribute: _SuperCharacter__power sc = SuperCharacter("Bob", 10, 5) print(sc.get_power()) # 10 — parent's __power is unchanged print(sc._SuperCharacter__power) # 15 — child's __power (accessing mangled name) # ── DUNDER METHODS ──────────────────────────────────────────────────────────── class Score: def __init__(self, value): self.value = value def __str__(self): return f"Score: {self.value}" def __add__(self, other): return Score(self.value + other.value) s1 = Score(10) s2 = Score(20) print(s1 + s2) # calls __add__ -> prints: Score: 30 # ── THROWAWAY UNDERSCORE ────────────────────────────────────────────────────── coordinates = [(1, 2), (3, 4), (5, 6)] for _, y in coordinates: # we don't need x print(f"y coordinate: {y}")
- _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.
// io.thecodeforge — python tutorial def validate_user_session(cached_obj, fresh_obj): # BAD: cached values might be different objects from fresh query if cached_obj is fresh_obj: return "Session valid" # Almost never hits # CORRECT: compare the actual data, not memory addresses if cached_obj == fresh_obj: return "Session valid" return "Session expired" # Real-world trap with small integers a = 256 b = 256 print(a is b) # True — cached by CPython a = 257 b = 257 print(a is b) # False — NOT cached, different objects # What you actually wanted: print(a == b) # True — always correct
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.
// io.thecodeforge — python tutorial import builtins def crash_reporting(): # This guy just broke every print() in this scope print = "dashboard_v2" # Oops, shadowed built-in try: # Next line crashes — 'print' is now a string print("Generating report...") except TypeError as error: # Recovery: use the original built-in builtins.print(f"Recovered from: {error}") builtins.print("Generating report...") crash_reporting() # Global scope example — don't do this: # list = [1, 2, 3] # Now list() is broken forever # my_data = list("abc") # TypeError
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 eval() or exec(), any valid identifier name becomes an attack vector. A user-controlled string containing 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()
// io.thecodeforge — python tutorial import ast # NEVER do this — user input goes straight to interpreter user_input = "__import__('os').system('whoami')" # Attacker payload # result = eval(user_input) # DANGER: this runs the os command def safe_config_value(raw_input: str): """Only parse literal Python objects — no code execution.""" # Whitelist what's allowed allowed_types = {int, float, str, bool, type(None), list, dict, tuple} try: parsed = ast.literal_eval(raw_input) if type(parsed) not in allowed_types: raise ValueError(f"Type {type(parsed)} not allowed in config") return parsed except (ValueError, SyntaxError) as e: raise ValueError(f"Config parse failed: {e}") # This works safely print(safe_config_value("[1, 2, 3]")) # Safe # This raises an error instead of executing code try: safe_config_value(user_input) except ValueError as err: print(f"Blocked attack: {err}")
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.python -c "print(type(X))" where X is the suspicious namegrep -rn '^list\b\|^str\b\|^int\b\|^print\b' *.pylist_of_ids). Restart the Python interpreter.Print all local/global names: `print(dir())` in the failing scopeInspect the traceback to see the exact line and column of the undefined name.| Aspect | Keywords | Identifiers |
|---|---|---|
| Who defines them? | Python itself — built into the language | You — the programmer decides the name |
| Can you change their meaning? | No — fixed forever, SyntaxError if misused | Yes — you define what they hold or do |
| Total count | Exactly 35 in Python 3.12 | Unlimited — as many as your program needs |
| Case sensitivity | Case matters: True ≠ true (true is NOT a keyword) | Case matters: score ≠ Score ≠ SCORE |
| Can start with a digit? | N/A — fixed names set by Python | No — '1score' is a SyntaxError |
| Can contain hyphens? | N/A | No — 'player-score' is a SyntaxError; use 'player_score' |
| PEP 8 convention | Already follow the spec — no choice | snake_case for vars/functions, PascalCase for classes, ALL_CAPS for constants |
| Checked at runtime? | No — checked at parse time (before code runs) | Partially — undefined identifiers raise NameError at runtime |
| Tool to inspect them | keyword.kwlist / keyword.iskeyword() | dir() shows all names in current scope |
| 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 |
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.What is the difference between a keyword and a built-in in Python?
for or if causes a SyntaxError at parse time, before the code ever runs.
Built-ins like list, print, and len are names defined in Python's built-in namespace. They are NOT keywords — keyword.iskeyword('list') returns False. You CAN assign to them as variable names, and the interpreter won't complain. That's precisely what makes them dangerous: shadowing a built-in doesn't cause a parse error, but it causes confusing runtime bugs.
To see all built-in names, run dir(__builtins__).Why does Python use 'is None' rather than '== None' when checking for None values?
is checks object identity — it compares whether two references point to the exact same object in memory. None is a singleton: there is exactly one None object in any running Python program. So x is None is a single pointer comparison — fast and unambiguous.
2. == calls the __eq__ method, which can be overridden by custom classes. A class could define __eq__ such that some_object == None returns True even though the object is not actually None. This would introduce subtle bugs. is cannot be overridden — it always compares identity.
PEP 8 mandates the use of is and is not for None comparisons. Follow it always.What happens if you rename a variable to 'True' (uppercase T) in a Python program?
True is a keyword — you cannot assign to it. Doing True = False raises a SyntaxError: cannot assign to True. Python explicitly prevents reassignment of keywords to protect the language's integrity. Note that true (lowercase) is NOT a keyword — it can technically be used as a variable name, though never do that in real code.Frequently Asked Questions
Python 3.12 has exactly 35 keywords. This number has changed across Python versions — Python 3.5 added 'async' and 'await', for example. You can always get the exact count for your version by running 'import keyword; print(len(keyword.kwlist))' in your Python environment.
Yes, 'None' is one of Python's 35 keywords — and it must be written with a capital N. It represents the complete absence of a value, similar to 'null' in other languages. 'none' (all lowercase) is not a keyword and could technically be used as a variable name, though doing so is a recipe for confusion.
A keyword is a word with a fixed, built-in meaning that Python's interpreter recognises before your code even runs — you cannot use keywords as variable names. An identifier is a name you choose for your own variables, functions, and classes — it follows Python's naming rules but its meaning is entirely up to you. Keywords are the language's vocabulary; identifiers are your vocabulary inside that language.
Yes, technically — 'print' is not a keyword (check with keyword.iskeyword('print') -> False). You CAN assign print = 42 without a SyntaxError. But doing so shadows the built-in function, and any later call to print()print(...) in that scope will raise TypeError: 'int' object is not callable. Never use built-in names as identifiers.
Run 1player = 5 — you'll get SyntaxError: invalid decimal literal. For a hyphen: player-score = 10 gives SyntaxError: cannot assign to operator here. Maybe you meant '==' instead of '='?. For a keyword: for = 2 gives SyntaxError: cannot assign to for. The message varies but always points to the line where the parser got confused.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Python Basics. Mark it forged?
7 min read · try the examples if you haven't