Junior 8 min · March 05, 2026

Python Keywords & Identifiers -- `list` Shadowing Crash

A variable named list triggered TypeError: 'list' object is not callable in production.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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 list or print is not a keyword violation but causes silent runtime bugs
✦ Definition~90s read
What is Python Keywords and Identifiers?

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.

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 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 id()) and == (equality comparison, checks __eq__), which burns teams when comparing singletons like None or sentinel objects.

Plain-English First

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.

explore_keywords.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)}")
Output
Python 3.12.0 has 35 keywords:
False None True and as
assert async await break class
continue def del elif else
except finally for from global
if import in is lambda
nonlocal not or pass raise
return try while with yield
Is 'while' a Python keyword? True
Is 'speed' a Python keyword? False
Pro Tip:
Bookmark 'keyword.iskeyword()' — use it whenever you're unsure whether a word you want to use as a variable name is reserved. One line check, zero guessing.
Production Insight
A junior dev once used return as a variable name in a large codebase — but return is a keyword, so Python threw a SyntaxError at the assignment line.
The build failed, but because the error message just said 'invalid syntax', the team spent 45 minutes hunting for missing parentheses before noticing the variable name.
Lesson: always run keyword.iskeyword() on any name that triggers an unexpected SyntaxError.
Key Takeaway
Keywords are the 35 words owned by Python — you can't use them as identifiers.
That capitalisation matters: True, False, None are the only uppercase ones.
If you get a SyntaxError and can't see why, check your variable names against keyword.kwlist.
Python Keywords & Identifiers: Shadowing Crash THECODEFORGE.IO Python Keywords & Identifiers: Shadowing Crash Flow from keyword rules to identifier pitfalls and name resolution Python Keywords Reserved words like if, else, for, while, def Identifier Rules Letters, digits, underscores; no keyword reuse Built-in Shadowing Assigning to list, dict, str breaks functionality Underscore Conventions _private, __mangled, __dunder__ Name Resolution (LEGB) Local, Enclosing, Global, Built-in order Safe Identifier Use Avoid shadowing; use distinct names ⚠ Shadowing list or dict causes silent crashes Never use built-in names as variable names THECODEFORGE.IO
thecodeforge.io
Python Keywords & Identifiers: Shadowing Crash
Python Keywords Identifiers

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__'.

identifier_rules_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ── 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
Output
Player: Alice
Score: 0
Max level: 100
Checkpoints: [1, 5, 9]
total_coins = 50
Total_Coins = 200
New player name: Bob
Watch Out:
Python won't stop you naming a variable 'l' (lowercase L) or 'O' (uppercase o) — but those look identical to '1' and '0' in many fonts. PEP 8 explicitly bans them as single-character names. Use 'index' or 'count' instead of 'l', and 'zero_value' or 'offset' instead of 'O'.
Production Insight
A team once had a bug where player1 and playerl were both used — the second uses lowercase L instead of digit 1.
The code ran fine until a logic error in a data pipeline used the wrong variable, causing corrupted player stats for months.
Lesson: never use single-character names like 'l' or 'O', and enforce PEP 8 naming in code reviews.
Key Takeaway
Identifiers follow four rules: letters/digits/underscores only, no leading digit, no keywords, no special chars.
Case sensitivity means score, Score, and SCORE are three different names.
Follow PEP 8: snake_case for variables, PascalCase for classes, ALL_CAPS for constants.

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.

keywords_by_category.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 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
Output
=== Game State ===
Game active: True
High score set: False
Last winner: None
=== Difficulty Check ===
Mid-game — stay sharp.
=== Inventory Scan ===
Equipped: sword
Equipped: shield
=== Lives Countdown ===
Lives remaining: 3
Lives remaining: 2
Lives remaining: 1
=== Scoring Functions ===
Bonus score: 450
Doubled score: 900
=== Safe Division ===
Division attempt complete.
300.0
Can't divide by zero players — game not started yet.
Division attempt complete.
None
=== Win Condition ===
You win! Both conditions met.
'map' in inventory: True
last_winner is None: True
Interview Gold:
Interviewers love asking the difference between 'is' and '=='. Here's the short answer: '==' checks if two values are equal. 'is' checks if two variables point to the exact same object in memory. 'None' should always be checked with 'is None', never '== None' — PEP 8 explicitly requires this because '==' can be overridden by custom classes, but 'is' cannot.
Production Insight
A developer once used is to compare integers: if value is 1000: — it failed sometimes because Python caches small integers (-5 to 256) but not larger ones.
The bug only showed in production with certain data sizes.
Lesson: use == for value comparisons; reserve is for singleton checks (None, True, False).
Key Takeaway
Keywords are grouped by purpose: values, control flow, functions, error handling, imports, scope, logic.
Each group serves one job — learning them by category is faster than memorising alphabetically.
The 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 list() will fail with 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.

shadowing_builtins.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# ── 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.")
Output
Processing 3 orders
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in process_orders
TypeError: 'list' object is not callable
(When running the fixed version, no error occurs.)
Critical:
Shadowed built-ins are the #1 cause of mysterious TypeError bugs in Python production codebases. They're hard to trace because the error often points to a different piece of code. Use flake8-builtins or PyLint's builtin-attribute-shadowing in your CI — they catch this before it ships.
Production Insight
A payment processing service crashed silently after a developer renamed a variable list in a helper function.
The crash only happened when certain orders triggered that code path, and it took the on-call team three hours to trace the traceback back to the shadowed name.
Lesson: treat built-in names as 'effectively reserved' — even though they're not keywords, never assign to them.
Key Takeaway
Built-in names like list, str, print are NOT keywords — Python lets you shadow them.
That shadowing causes silent runtime bugs that are hard to debug.
Always rename variables descriptively and add a builtin-shadowing lint rule to your CI.

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'.

underscore_conventions.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# ── 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}")
Output
100
10
15
Score: 30
y coordinate: 2
y coordinate: 4
y coordinate: 6
Mental Model: Underscore as a 'Trust Signal'
  • _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.
Production Insight
A developer used __init__ as a method name (not the constructor) thinking double underscores made it 'private'.
Python's interpreter treated it as name mangling, and the method became _ClassName__init__, breaking all calls to it.
Lesson: never use dunder names for your own purposes — they're reserved by Python.
Key Takeaway
Single leading underscore: convention for internal use, not enforced.
Double leading underscore: triggers name mangling — use for avoiding subclass conflicts, not for access control.
Double underscore both sides (dunder): for Python's special methods only — never invent your own.

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.

IdentitySnare.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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
Output
True
False
True
Production Trap:
Never use is for comparing strings from user input, API responses, or database reads. == is your friend. is is a loaded weapon.
Key Takeaway
Use 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 print() dies with TypeError: 'str' object is not callable. I've seen this in production because someone used list as a variable name, then tried to call list() on a response payload.

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.

LegbShadow.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
Output
Recovered from: 'str' object is not callable
Generating report...
Senior Shortcut:
Run pylint --disable=all --enable=redefined-builtin on your codebase. It catches every shadowed built-in before your tests run.
Key Takeaway
Python resolves names by searching local → enclosed → global → built-in scope. Never shadow built-ins with variable names. Use 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 eval() because it was "clean code." Congratulations, you just handed your server keys to anyone who can post __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 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 eval() 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.

EvalNightmare.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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}")
Output
[1, 2, 3]
Blocked attack: malformed node or string: <class '_ast.Call'>
Production Trap:
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.
Key Takeaway
Treat 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.
● Production incidentPOST-MORTEMseverity: high

The Silent Crash: How Shadowing `list` Broke Production

Symptom
The order-processing pipeline started throwing TypeError: 'list' object is not callable on a line that called list(product_ids). The code had been working for months.
Assumption
The team assumed a corrupted database or a library upgrade had changed the behaviour of the list constructor.
Root cause
A new developer had used 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.
Fix
Rename the variable from 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.
Key lesson
  • 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-builtins or PyLint's builtin-attribute-shadowing check to your CI pipeline — it catches these before they reach production.
Production debug guideSymptom → Action guide for naming errors in Python codebases4 entries
Symptom · 01
SyntaxError on a line that looks correct (e.g., while True: fails)
Fix
Check capitalisation: keywords are all lowercase except True, False, None. While with capital W is not a keyword. Use syntax highlighting — it will show keyword colour vs identifier colour.
Symptom · 02
TypeError: 'xxx' object is not callable
Fix
Search your codebase for variable assignments to names like list, str, int, print, input. Run type(list) in the failing module — if it returns <class 'list'> incorrectly, someone shadowed it.
Symptom · 03
NameError: name 'xxx' is not defined
Fix
Check for typos in identifier names (case-sensitive). Use your IDE's 'Find References' to verify the name matches exactly. Also check if the name is defined inside a function but accessed outside its scope.
Symptom · 04
ImportError after changing a module name
Fix
Module names are identifiers too — they follow the same rules. If the module file contains a hyphen or starts with a digit, importing it fails. Rename the file to use underscores.
★ Quick Identifier Debug Cheat SheetWhen your code won't run or behaves unexpectedly, run these checks in order.
SyntaxError at parse time — code never runs
Immediate action
Look for keywords used as names, hyphens in identifiers, or leading digits.
Commands
python -c "import keyword; print(keyword.iskeyword('your_word'))"
Check syntax highlighting in your editor — keywords are coloured differently.
Fix now
Rename the identifier: replace hyphens with underscores, add a letter before a leading digit.
TypeError: 'X' object is not callable+
Immediate action
Check if you've used a built-in name as a variable.
Commands
python -c "print(type(X))" where X is the suspicious name
grep -rn '^list\b\|^str\b\|^int\b\|^print\b' *.py
Fix now
Rename the conflicting variable to something descriptive (e.g., list_of_ids). Restart the Python interpreter.
NameError: name not defined+
Immediate action
Check spelling and case — Python is case-sensitive.
Commands
Print all local/global names: `print(dir())` in the failing scope
Inspect the traceback to see the exact line and column of the undefined name.
Fix now
Correct the spelling or define the variable before use.
Keywords vs Identifiers at a Glance
AspectKeywordsIdentifiers
Who defines them?Python itself — built into the languageYou — the programmer decides the name
Can you change their meaning?No — fixed forever, SyntaxError if misusedYes — you define what they hold or do
Total countExactly 35 in Python 3.12Unlimited — as many as your program needs
Case sensitivityCase matters: True ≠ true (true is NOT a keyword)Case matters: score ≠ Score ≠ SCORE
Can start with a digit?N/A — fixed names set by PythonNo — '1score' is a SyntaxError
Can contain hyphens?N/ANo — 'player-score' is a SyntaxError; use 'player_score'
PEP 8 conventionAlready follow the spec — no choicesnake_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 themkeyword.kwlist / keyword.iskeyword()dir() shows all names in current scope

Key takeaways

1
Python has exactly 35 keywords
they are owned by the language, never by you. Use 'keyword.iskeyword()' to check any word you're unsure about before using it as a name.
2
Identifiers follow four hard rules
letters/digits/underscores only, can't start with a digit, can't be a keyword, no spaces or special characters. Break any one rule and Python won't even start running your code.
3
Python is strictly case-sensitive in both keywords and identifiers
'while' is a keyword, 'While' is a valid (but terrible) variable name; 'player_score' and 'Player_Score' are two completely separate variables.
4
Built-ins like 'list', 'print', 'str', and 'input' are NOT keywords but shadowing them by using them as variable names causes silent, hard-to-debug runtime failures
treat them as if they were reserved.
5
Underscore conventions matter
_single means 'internal use', __double triggers name mangling, __dunder__ is for Python's special methods only.

Common mistakes to avoid

4 patterns
×

Using a built-in name like 'list', 'input', 'print', or 'str' as a variable name

Symptom
Your code silently stops working or throws 'TypeError: 'list' object is not callable'. The error appears in a completely different part of the code, making it hard to trace back to the variable assignment.
Fix
Rename your variable to something descriptive like 'player_list' or 'score_list'. Add 'flake8-builtins' to your project's linter configuration to catch these automatically.
×

Using hyphens in variable names instead of underscores

Symptom
SyntaxError on a line like 'player-score = 100'. Python interprets the hyphen as a minus operator, so it tries to evaluate 'player minus score = 100', which makes no sense.
Fix
Always use underscores: 'player_score = 100'. If you're coming from CSS or HTML, retrain your muscle memory — Python only accepts underscores.
×

Assuming Python keywords are case-insensitive

Symptom
Using 'While', 'IF', or 'TRUE' expecting them to work like keywords — 'While True:' raises a SyntaxError because 'While' with a capital W is not a keyword; only 'while' (lowercase) is. The three exceptions are 'True', 'False', and 'None' which must be capitalised exactly as written.
Fix
Always type keywords in lowercase except for those three — your code editor's syntax highlighting will confirm whether you've got it right.
×

Using lowercase 'true', 'false', or 'none' as variable names

Symptom
These are not keywords — Python accepts them as variable names. But other developers will assume you made a typo, and your code will look amateurish. Worse, if you later try to use True (uppercase) in the same scope, you'll have two different values causing logical errors.
Fix
Use the correct capitalisation for boolean constants and None. If you accidentally used lowercase, rename the variable immediately — True, False, None are the only valid forms.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Can you name five Python keywords and explain what each one does?
Q02SENIOR
What is the difference between a keyword and a built-in in Python?
Q03SENIOR
Why does Python use 'is None' rather than '== None' when checking for No...
Q04JUNIOR
What happens if you rename a variable to 'True' (uppercase T) in a Pytho...
Q01 of 04JUNIOR

Can you name five Python keywords and explain what each one does?

ANSWER
Sure. Here are five with their primary purpose: - 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How many keywords does Python have?
02
Is 'None' a keyword in Python?
03
What is the difference between a Python keyword and an identifier?
04
Can I use 'print' as a variable name in Python?
05
What does a syntax error for an invalid identifier look like?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Python Basics. Mark it forged?

8 min read · try the examples if you haven't

Previous
Python Indentation and Syntax
10 / 17 · Python Basics
Next
Python vs Other Languages