Home Python Python Variables — NameError That Broke a Payment Pipeline
Beginner 7 min · March 05, 2026

Python Variables — NameError That Broke a Payment Pipeline

10% of payments crashed with NameError because total_charges was only defined in an if block.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 20 min
  • Basic programming fundamentals
  • A computer with internet access
  • Willingness to follow along with examples
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Core concept: variables are named references to objects in memory, not containers
  • Assignment (=) binds a name to a value; the name points to the object
  • Dynamic typing: the same name can refer to different types over time
  • Performance: variable lookup is O(1) dictionary access in local/global scope
  • Production risk: NameError crashes pipelines when variable accessed before assignment
  • Biggest mistake: using = when you meant ==, causing a SyntaxError in conditions
✦ Definition~90s read
What is Variables in Python?

A Python variable is a name bound to an object in memory — a reference, not a box that holds a value. Unlike C or Java, Python variables have no type; the objects they point to do. This dynamic typing is why you can reassign x = 42 then x = 'hello' without a compiler error.

Think of a variable like a labelled box in a storage room.

It’s also why a single typo in a payment pipeline — like total vs totl — raises a NameError at runtime, not at compile time, potentially costing real money. Python’s simplicity here is a double-edged sword: zero boilerplate for quick scripts, but zero safety nets for production systems.

Variables live in namespaces (local, enclosing, global, built-in) resolved via the LEGB rule. When you assign inside a function, Python creates a local variable by default — unless you declare global or nonlocal. This scoping behavior is cheap in memory: Python uses a dict (or optimized array in CPython 3.12+) per scope, with reference counting for cleanup.

For high-throughput pipelines, avoid global — it bypasses fast local lookups and introduces hidden coupling. Instead, pass state explicitly or use closures.

Type hinting (total: float = 0.0) doesn’t enforce types at runtime, but tools like mypy and pyright catch mismatches before deployment. In a payment system, a variable meant to hold a Decimal (for currency) accidentally receiving a float due to a missing cast can silently truncate cents.

Type hints, combined with runtime checkers like pydantic, turn Python’s variable model from a liability into a predictable contract. Use them from day one — retrofitting is painful.

Plain-English First

Think of a variable like a labelled box in a storage room. You write a name on the outside of the box (the variable name), then you put something inside it (the value). Whenever you need that thing later, you just call out the label and Python hands it back to you. The magic part? You can swap what's inside the box at any time without changing the label.

Every app you've ever used — from Instagram to Google Maps — is constantly juggling data. A username here, a distance there, a price, a temperature, a score. Without a way to temporarily hold that data while the program is running, none of it would be possible. Variables are the most fundamental building block of every program ever written, in every language that exists.

But here's the thing Python does differently: it doesn't force you to declare types. You just pick a name, assign a value, and Python figures out the rest. That simplicity is what makes Python the go-to language for beginners and a productivity powerhouse for pros. Get variables right, and everything else becomes easier to reason about.

What a Variable Actually Is (And Why Python Makes Them Effortless)

In most older languages like C or Java, you have to tell the computer upfront exactly what kind of data you're planning to store — a number, a word, a decimal — before you can even create the variable. Python throws that ceremony out the window. You just pick a name, use the equals sign, and put your value on the right. Python figures out the type automatically. That equals sign is called the assignment operator. It doesn't mean 'equal to' the way it does in maths — it means 'take the value on the right and store it under the name on the left'. So when you write player_score = 0, you're telling Python: create a box, label it player_score, and put the number 0 inside it. From that line forward, every time Python sees player_score, it looks inside that box and uses whatever's in there. This simplicity is intentional. Python's creator, Guido van Rossum, wanted the language to read almost like plain English, and variable assignment is the clearest example of that philosophy in action.

variable_basics.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ── Creating variables ──────────────────────────────────────────
# On the left: the label (variable name)
# On the right: the value you want to store
# The = sign means ASSIGN, not 'is equal to'

player_name = "Alex"          # Storing a piece of text (called a string)
player_score = 0              # Storing a whole number (called an integer)
health_percentage = 100.0     # Storing a decimal number (called a float)
is_game_over = False          # Storing True or False (called a boolean)

# ── Reading variables ────────────────────────────────────────────
# Use print() to display the value stored inside any variable
print(player_name)            # Python looks inside the box labelled player_name
print(player_score)
print(health_percentage)
print(is_game_over)

# ── Updating a variable ──────────────────────────────────────────
# You can replace the value inside the box at any time
player_score = 50             # The old value (0) is gone; now it holds 50
print(player_score)           # Prints the NEW value
Output
Alex
0
100.0
False
50
🔥Why This Matters:
Python determines the data type automatically based on the value you assign. This is called dynamic typing. You don't write 'string player_name' — you just write 'player_name = "Alex"' and Python knows it's text. This is one of the biggest reasons Python is the most beginner-friendly language in the world.
📊 Production Insight
Dynamic typing reduces boilerplate but shifts type errors to runtime.
A simple typo like 'score = scpre 10' will raise NameError, not compile-time error.
Rule: test immediately after assigning any variable – don't wait until the code runs 50 lines later.
🎯 Key Takeaway
Dynamic typing does not mean no typing.
Every variable has a type – Python just figures it out when the line runs.
Always know what type your variable holds, especially when passing it to functions.
variables-python Python Variable Scope Hierarchy LEGB rule: Local, Enclosing, Global, Built-in layers Built-in Scope print | len | range Global Scope module-level variables | constants | functions Enclosing Scope outer function variables | nonlocal declarations Local Scope function parameters | local variables THECODEFORGE.IO
thecodeforge.io
Variables Python

The Rules and Best Practices for Naming Python Variables

Python gives you a lot of freedom with variable names, but there are hard rules you cannot break and soft rules (conventions) that every professional follows. Breaking the hard rules causes an immediate crash. Breaking the conventions means your colleagues will quietly judge you. Hard rules: variable names can only contain letters, numbers, and underscores. They cannot start with a number. They cannot contain spaces. They cannot be one of Python's reserved keywords (like if, for, while, return — these mean something special to Python already). Case matters — age, Age, and AGE are three completely different variables in Python's eyes. The professional convention used across almost all Python code is called snake_case: all lowercase letters, with words separated by underscores. So instead of PlayerHealthPoints, you'd write player_health_points. This is defined in PEP 8 — Python's official style guide — and every serious Python codebase follows it. Good names are the cheapest form of documentation that exists. A variable named d tells you nothing. A variable named days_until_deadline tells you everything.

variable_naming.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
# ── VALID variable names ─────────────────────────────────────────
user_age = 28                 # snake_case — the Python standard
city_of_birth = "Lagos"       # descriptive and readable
max_login_attempts = 5        # immediately clear what this stores
order_total_usd = 149.99      # units in the name — no ambiguity
_internal_counter = 0         # leading underscore = convention for internal use

# ── INVALID variable names (these will crash your program) ───────
# 2fast = True                # WRONG: cannot START with a number
# user-age = 28               # WRONG: hyphens are not allowed
# user age = 28               # WRONG: spaces are not allowed
# for = 5                     # WRONG: 'for' is a reserved Python keyword

# ── Case sensitivity demo ────────────────────────────────────────
temperature = 36.6            # This is one variable...
Temperature = 100.0           # ...and this is a COMPLETELY different variable
TEMPERATURE = -273.15         # ...and so is this — Python treats them separately

print(temperature)            # 36.6
print(Temperature)            # 100.0
print(TEMPERATURE)            # -273.15

# ── Name quality comparison ──────────────────────────────────────
d = 7                         # BAD  — what is d? Days? Dollars? Distance?
days_until_expiry = 7         # GOOD — crystal clear, zero guessing required
Output
36.6
100.0
-273.15
⚠ Watch Out:
Never name a variable after a Python built-in function like list, input, print, or type. Python will let you do it without crashing immediately — but you'll overwrite that built-in, and the next time you try to call print() or list() it will behave in completely unexpected ways. This is one of the sneakiest bugs beginners introduce.
📊 Production Insight
A misnamed variable can silently overwrite a built-in, breaking code far from the assignment.
When you shadow 'list', calls to list() later will fail with TypeError.
Rule: before naming, check if it's a built-in — your IDE will colour it differently.
🎯 Key Takeaway
A good variable name is the cheapest documentation.
Follow snake_case, avoid reserved words, and never reuse built-in names.
Your future self (and code reviewers) will thank you.

Multiple Assignment, Swapping Values, and Checking Types

Python has a few elegant shortcuts for working with variables that feel almost like magic when you first see them. Multiple assignment lets you create several variables on a single line. You can also assign the same value to multiple variables at once — useful when initialising a group of counters to zero. Then there's the crown jewel: swapping two variables. In most other languages, swapping the values of two variables requires a third temporary variable to act as a middleman. In Python, you do it in one line. Under the hood Python is still using a temporary location, but it hides that complexity from you entirely. Finally, Python gives you the type() function, which tells you exactly what kind of data a variable is currently holding. This is especially useful when you're debugging and something isn't behaving the way you expect — checking the type is often the fastest way to spot the problem.

advanced_assignment.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
# ── Assigning multiple variables in one line ────────────────────
# Each name on the left pairs up with the matching value on the right
first_name, last_name, age = "Maria", "Santos", 31
print(first_name)             # Maria
print(last_name)              # Santos
print(age)                    # 31

# ── Assigning the same value to multiple variables at once ───────
red_lives = blue_lives = green_lives = 3   # All three teams start with 3 lives
print(red_lives, blue_lives, green_lives)  # 3 3 3

# ── Swapping two variables — the Python way ──────────────────────
team_a_score = 45
team_b_score = 72

print(f"Before swap — Team A: {team_a_score}, Team B: {team_b_score}")

# In one line, Python swaps the values with no temporary variable needed
team_a_score, team_b_score = team_b_score, team_a_score

print(f"After swap  — Team A: {team_a_score}, Team B: {team_b_score}")

# ── Checking what type of data a variable holds ──────────────────
product_name = "Wireless Keyboard"   # text
product_price = 49.99                # decimal
units_in_stock = 200                 # whole number
is_available = True                  # boolean

print(type(product_name))            # what type is this variable?
print(type(product_price))
print(type(units_in_stock))
print(type(is_available))
Output
Maria
Santos
31
3 3 3
Before swap — Team A: 45, Team B: 72
After swap — Team A: 72, Team B: 45
<class 'str'>
<class 'float'>
<class 'int'>
<class 'bool'>
💡Pro Tip:
The f-string syntax you see in the swap example — f"Team A: {team_a_score}" — is the modern, preferred way to embed variable values directly inside strings. The f before the opening quote tells Python to look for anything inside curly braces and replace it with the variable's actual value. It's cleaner and faster than joining strings with + signs.
📊 Production Insight
Multiple assignment on one line can cause order-dependent bugs if the right side mutates shared state.
Example: a, b = some_list, some_list.pop() — the pop happens during the tuple construction, not after.
Rule: when using multiple assignment, ensure right-hand expressions are independent and side-effect free.
🎯 Key Takeaway
Python's tuple packing and unpacking makes multiple assignment elegant.
One-line swaps avoid temporary variables but don't sacrifice readability.
Use type() early when debugging unexpected behaviour — it reveals most type mismatches quickly.
variables-python Variable Assignment vs. Mutation How rebinding differs from modifying mutable objects Assignment (Rebinding) Mutation (In-place) Operation x = [1, 2] x.append(3) Memory Effect Creates new object, x points to it Modifies existing object in place Scope Impact Can change which object a name refers to Does not change the reference; affects a Common Trap Unexpected NameError if name not defined Unexpected side effects when sharing mut Example a = b = [] then a = [1] — b unchanged a = b = [] then a.append(1) — b also cha THECODEFORGE.IO
thecodeforge.io
Variables Python

Variable Scope: Local vs Global – and What Python Actually Does in Memory

Where you create a variable determines how long it lives and who can see it. Variables defined inside a function are local — they exist only while that function runs. Variables defined at the top level of a script are global — they exist for the entire program. Python resolves names by searching in this order: local, enclosing function, global, built-in (LEGB rule). This matters because you can accidentally shadow a global variable inside a function by assigning to it, which creates a new local instead of modifying the global. To explicitly modify a global from inside a function, you use the global keyword. Objects themselves can be mutable (like lists) even when assigned to a global name — so you can modify the list's contents without using global. Memory-wise, each variable name is a key in a dictionary: locals() for local scope, globals() for global scope. This dictionary lookup is fast but not instant — Python caches local variables in a dedicated array for faster access.

variable_scope.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
# ── Global variable ────────────────────────────────────────────
user_count = 0                # This exists for the entire program

# ── Local variable inside a function ─────────────────────────────
def process_user(name):
    local_greeting = f"Hello, {name}"   # local_greeting only exists inside this function
    user_count = 1                        # This creates a LOCAL variable, does NOT modify global
    print(local_greeting)
    print(f"Inside function, user_count = {user_count}")

process_user("Alice")
print(f"Outside function, user_count = {user_count}")  # Still 0!

# ── Using global keyword to modify the global ────────────────────
def increment_user_count():
    global user_count                    # Tell Python: use the global user_count
    user_count += 1

increment_user_count()
increment_user_count()
print(f"After global increment: {user_count}")  # 2

# ── Modifying a mutable global without global keyword ────────────
active_users = ["Bob"]          # global list
def add_user(name):
    active_users.append(name)   # No global keyword needed because we're modifying, not reassigning

add_user("Charlie")
print(active_users)             # ['Bob', 'Charlie']
Output
Hello, Alice
Inside function, user_count = 1
Outside function, user_count = 0
After global increment: 2
['Bob', 'Charlie']
Mental Model
Scope Mental Model
Think of variable scope like concentric circles with different visibility rules.
  • Inner circle: local function variables (most specific, looked up first)
  • Next circle: enclosing function variables (for nested functions)
  • Next circle: global module-level variables
  • Outer circle: Python built-ins like print, len, range
  • Python circles inward: LEGB rule saves lookup time by starting from the centre
📊 Production Insight
The global keyword is often a code smell – it makes function behaviour depend on external state.
In large codebases, unexpected global modifications cause hard-to-reproduce bugs across modules.
Prefer passing values as function arguments and returning results instead of relying on globals.
🎯 Key Takeaway
Variables are scoped: local inside functions, global at module level.
Assigning to a variable inside a function creates a local unless you declare 'global'.
Modifying an object (like list.append) does not require global – only reassigning the name does.

Constants in Python – The Convention That Replaces the Keyword

Unlike many languages, Python doesn't have built-in constant declarations — no const or final keyword. Instead, the Python community follows a naming convention: constants are written in ALL_CAPS with underscores separating words. This signals to other developers: 'this value should not change.' The interpreter won't stop you from reassigning a constant — it's purely a convention. Violations are caught in code review, not by the compiler. This approach works surprisingly well in practice because Python trusts developers to follow the convention. For truly immutable values, you can use tuples or namedtuples (if they contain only immutable elements). When you see PI = 3.14159 at the top of a module, you know it's a constant. If someone later writes PI = 4, the code will still run, but your colleagues will have strong words with you.

constants.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
# ── Constants are named using ALL_CAPS ───────────────────────────
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
PI = 3.141592653589793
SUPPORTED_REGIONS = ("us-east", "eu-west", "ap-southeast")

# ── Python does NOT prevent reassignment — use with care ─────────
# MAX_RETRIES = 5   # This would violate the convention, but Python allows it

# ── Using constants keeps business logic clear ───────────────────
import time
def fetch_data(url, retries=MAX_RETRIES):
    for attempt in range(retries):
        try:
            response = make_request(url)
            return response
        except ConnectionError:
            if attempt == retries - 1:
                raise
            time.sleep(DEFAULT_TIMEOUT)

# ── Constants grouped in a dedicated module are best practice ────
# config.py
#   MAX_CONNECTIONS = 100
#   DATABASE_HOST = "db.prod.example.com"
Output
(no output — module definition)
⚠ Watch Out:
Constants defined with mutable objects (like a list) can be modified even if you don't reassign the name. For example, SUPPORTED_REGIONS.append('new-region') will modify the tuple… wait, tuples are immutable – but if you used a list, you could accidentally change its contents. Always use immutable types for constants if you want them to stay constant.
📊 Production Insight
Relying on naming convention alone means a careless developer can mutate a constant, causing subtle bugs.
A common pattern is to enforce immutability by using namedtuple or dataclass(frozen=True).
For true safety in large teams, consider a static type checker like mypy with Final type annotation.
🎯 Key Takeaway
Python constants are a convention, not a language feature.
Use ALL_CAPS names and never reassign them – enforce in code reviews.
For immutable groups, use tuples or frozen dataclasses, never lists.

Why Python Variables Can Suddenly Break (And How Type Hinting Saves Your Ass)

Python is dynamically typed. That means a variable can switch from holding an int to a string to a database connection — and Python won't complain until runtime. Your production service doesn't care about your feelings; it cares that order.total is suddenly a string when you try to multiply it by 1.2. This is a class of bug that silently corrupts data for hours before anyone notices. Type hints don't enforce anything at runtime by default, but they give your editor and linter the ammo they need to catch mismatches before code hits production. Add from __future__ import annotations at the top of every file for deferred evaluation (no more circular import errors). Static analysis tools like mypy will then kill the build on type violations. Do this. Future you will not have to debug a billing loop that's concatenating floats.

TypeHintBreakdown.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — python tutorial

from __future__ import annotations
from decimal import Decimal


def apply_discount(price: Decimal, percent: float) -> Decimal:
    # mypy catches if you pass a string here
    return price * (Decimal(1) - Decimal(str(percent)))


# This looks fine, but if someone passes percent as a string:
# mypy: Argument 2 to "apply_discount" has incompatible type "str"
# Without the hint, this silently fails at runtime
order_total = Decimal("49.99")
print(f"Discounted: ${apply_discount(order_total, 0.15):.2f}")
Output
Discounted: $42.49
⚠ Production Trap:
Type hints are not runtime enforcement. Use mypy --strict in CI/CD or you're just writing documentation.
🎯 Key Takeaway
Type hints are a contract between you and your future self. Enforce them with a static analyser before production melts down.

The `is` Operator Isn't a Cooler `==` — It Will Burn You on Variable Identity

Here's a bug that wasted a senior dev's entire Friday. Two variables both held the value 256. a == b returned True. So they used a is b in a cache-key check. It worked in dev. In production, it randomly failed. Why? Python caches small integers (-5 to 256) and reuses memory; any int outside that range gets a new object each assignment. is compares memory addresses, not values. Never use is for value comparison. Use it only for singletons (None, True, False). The CPython internals are an implementation detail — they changed once already. If you write if name is 'admin', you're gambling that Python interned that string. Some do, some don't. Your code should not depend on the whims of the interpreter's optimisation pass.

IdentityCrisis.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — python tutorial

# Small int caching: -5 to 256 gets same memory address
small_a = 256
small_b = 256
print(f"small_a is small_b: {small_a is small_b}")  # True (cached)

large_a = 257
large_b = 257
print(f"large_a is large_b: {large_a is large_b}")  # False (new objects)

# Strings: some are interned, some aren't
name_1 = "id_admin"
name_2 = "id_" + "admin"
print(f"name_1 is name_2: {name_1 is name_2}")  # False (runtime concat)

# Correct check for None
user = None
if user is None:
    print("User is none — safe use of is")
Output
small_a is small_b: True
large_a is large_b: False
name_1 is name_2: False
User is none — safe use of is
⚠ Senior Shortcut:
Mentally replace is with id == id. Only use it for singletons. Everything else: use ==.
🎯 Key Takeaway
is checks memory identity, not value equality. Python's integer and string caching is an implementation detail — never rely on it.

Variable Mutation Traps: When Assigning a List Doesn't Copy It, It Ghosts You

Junior devs treat = like it's a data duplicator. Nope. In Python, b = a doesn't copy a — it copies the reference. Both names point to the same object in memory. Mutate via b, and a changes too. If a was a config list passed to three functions, now all three are seeing the same mutated state. You'll trace a bug for hours before realising the default_headers list grew a junk entry in function two that broke function three. The fix: always explicitly copy mutable objects when you intend to decouple them. Use copy.copy() for shallow copies, copy.deepcopy() for nested structures. Or, for lists, the slice shorthand b = a[:]. For dicts: b = a.copy(). If you're passing mutable defaults to functions, use None and instantiate inside — otherwise that default list persists across calls and accumulates state.

MutationAmbush.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
// io.thecodeforge — python tutorial

# WRONG: shared reference mutates original
original_config = ["host", "port"]
user_config = original_config
user_config.append("secret")
print(f"Original: {original_config}")  # Mutated!

# RIGHT: explicit copy
original_config = ["host", "port"]
user_config = original_config[:]  # slice copy
user_config.append("secret")
print(f"Original: {original_config}")  # Clean
print(f"User: {user_config}")

# Function default trap:
def add_header(headers: list = None):
    if headers is None:
        headers = []
    headers.append("X-Debug")
    return headers

print(add_header())  # ['X-Debug']
print(add_header())  # ['X-Debug'] not ['X-Debug', 'X-Debug']
Output
Original: ['host', 'port', 'secret']
Original: ['host', 'port']
User: ['host', 'port', 'secret']
['X-Debug']
['X-Debug']
🔥Production Trap:
Mutable default arguments are evaluated once at function definition time, not each call. Use None and instantiate inside the function body.
🎯 Key Takeaway
Assignment copies references, not data. If you need an independent copy, explicitly use copy, slice, or dict.copy().

Python Name Binding Model: Variables Are References

In Python, variables are not containers that store values; they are names that reference objects in memory. This is called the name binding model. When you write x = 5, Python creates an integer object 5 and binds the name x to it. If you then write y = x, Python does not copy the object; it binds the name y to the same object. This is why changes through one name can affect the other if the object is mutable. Understanding this model is crucial for debugging unexpected behavior, especially in large codebases. For example, consider:

a = [1, 2, 3] b = a b.append(4) print(a) # Output: [1, 2, 3, 4]

Here, a and b reference the same list object. Modifying the list through b also affects a. This is not a bug but a consequence of the name binding model. In contrast, immutable objects like integers or strings cannot be changed in place, so reassignment creates a new object. For instance:

x = 10
y = x
x = 20
print(y)  # Output: 10

Here, x is rebound to a new object, but y still references the original 10. This model is efficient because it avoids unnecessary copying, but it requires careful handling of mutable objects. In production, always be aware that assignment does not create a copy; use copy.copy() or copy.deepcopy() when you need independent copies.

name_binding.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Example of name binding with mutable objects
a = [1, 2, 3]
b = a
b.append(4)
print(a)  # [1, 2, 3, 4]

# Example with immutable objects
x = 10
y = x
x = 20
print(y)  # 10

# To create an independent copy
import copy
c = copy.deepcopy(a)
c.append(5)
print(a)  # [1, 2, 3, 4]
print(c)  # [1, 2, 3, 4, 5]
⚠ Assignment Does Not Copy
📊 Production Insight
In production, always use copy.deepcopy() for nested mutable structures to avoid unintended side effects. For simple lists, list.copy() or slicing [:] creates a shallow copy.
🎯 Key Takeaway
Python variables are names bound to objects; assignment creates a new reference, not a copy. This model is efficient but requires caution with mutable objects.

Mutable vs Immutable: Impact on Variable Assignment

Understanding mutability is key to predicting how variable assignments behave in Python. Immutable objects (e.g., int, float, str, tuple, frozenset) cannot be changed after creation. Any operation that seems to modify them actually creates a new object. For example:

``python s = "hello" s += " world" # Creates a new string object print(s) # "hello world" ``

Here, the original string "hello" is unchanged; s is rebound to a new string. In contrast, mutable objects (e.g., list, dict, set, custom classes) can be modified in place. Consider:

``python lst = [1, 2, 3] lst.append(4) # Modifies the list in place print(lst) # [1, 2, 3, 4] ``

This distinction affects how variables behave when passed to functions. For immutable objects, the function receives a reference to the same object, but since the object cannot be changed, the original variable remains unaffected. For mutable objects, the function can modify the object, affecting the original. Example:

```python def add_element(lst, element): lst.append(element)

my_list = [1, 2] add_element(my_list, 3) print(my_list) # [1, 2, 3] ```

To avoid unintended modifications, pass a copy or use immutable alternatives like tuples. In production, always document whether a function mutates its arguments, and consider using type hints to indicate mutability expectations.

mutable_vs_immutable.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Immutable example
s = "hello"
s += " world"
print(s)  # "hello world"

# Mutable example
lst = [1, 2, 3]
lst.append(4)
print(lst)  # [1, 2, 3, 4]

# Function modification
my_list = [1, 2]
def add_element(lst, element):
    lst.append(element)
add_element(my_list, 3)
print(my_list)  # [1, 2, 3]

# Avoiding mutation by passing a copy
new_list = my_list.copy()
add_element(new_list, 4)
print(my_list)  # [1, 2, 3]
🔥Mutable vs Immutable Recap
📊 Production Insight
In production, prefer immutable data structures for shared state to avoid unintended mutations. Use @dataclass(frozen=True) for immutable data containers.
🎯 Key Takeaway
Mutability determines whether an operation changes the original object or creates a new one. This affects variable assignment and function side effects.

Type Annotations for Variables in Modern Python

Type annotations (introduced in Python 3.5) allow you to specify the expected type of a variable, improving code readability and enabling static type checking with tools like mypy. Although Python remains dynamically typed, annotations serve as documentation and catch type-related bugs early. For example:

``python name: str = "Alice" age: int = 30 prices: list[float] = [19.99, 24.99] ``

Annotations are optional and do not affect runtime behavior. However, they become powerful when combined with type hints for function parameters and return values:

``python def greet(name: str) -> str: return f"Hello, {name}" ``

Modern Python also supports more complex types via the typing module, such as Optional, Union, List, Dict, and Any. For example:

```python from typing import Optional, List

def find_user(user_id: int) -> Optional[str]: # Returns username or None ... ```

In Python 3.9+, you can use built-in generic types like list[int] instead of List[int]. For Python 3.10+, X | Y syntax replaces Union[X, Y]. Type annotations help prevent the "NameError that broke a payment pipeline" by making variable expectations explicit. For instance, if a variable is expected to be a string but receives an integer, mypy will flag it. In production, integrate type checking into your CI pipeline to catch mismatches before deployment.

type_annotations.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
# Basic type annotations
name: str = "Alice"
age: int = 30
prices: list[float] = [19.99, 24.99]

# Function annotations
def greet(name: str) -> str:
    return f"Hello, {name}"

# Using typing module
from typing import Optional, List, Union

def find_user(user_id: int) -> Optional[str]:
    # Implementation
    return None

# Python 3.10+ union syntax
def process(value: int | str) -> None:
    print(value)

# Type alias
Vector = list[float]
def scale(vec: Vector, factor: float) -> Vector:
    return [x * factor for x in vec]
💡Start Type Hinting Gradually
📊 Production Insight
In production, enforce type checking with mypy in CI. Use --strict mode for maximum safety. Consider using Pydantic for runtime validation of data models.
🎯 Key Takeaway
Type annotations improve code clarity and enable static type checking, preventing type-related bugs. They are optional but highly recommended for production code.
● Production incidentPOST-MORTEMseverity: high

The NameError That Took Down a Payment Pipeline

Symptom
The payment pipeline would start processing orders, then crash with a NameError: name 'total_charges' is not defined after approximately 10% of transactions.
Assumption
The developer assumed that total_charges was implicitly initialised to 0 because it was referenced inside an accumulating loop. They believed Python would create the variable automatically upon first assignment in the loop body.
Root cause
The variable total_charges was assigned inside an if block that only executed for premium orders. For standard orders, the if block was skipped, leaving the variable never defined. When the code later tried to log total_charges, Python raised a NameError.
Fix
Initialise total_charges = 0 at the start of each order loop, before any conditional logic. This guarantees the variable always exists regardless of the order type.
Key lesson
  • Never assume a variable will be defined by a code path you can't guarantee executes.
  • Always initialise variables to a sensible default (0, 0.0, '', or None) before branches or loops.
  • Add defensive checks: if 'total_charges' not in locals(): total_charges = 0
Production debug guideQuick reference for diagnosing common variable runtime errors4 entries
Symptom · 01
NameError: name 'x' is not defined
Fix
Check if the variable was assigned before this line. Look for typos in the name. Ensure the variable is in scope (local vs global). Use print(dir()) to list local names.
Symptom · 02
UnboundLocalError: local variable 'x' referenced before assignment
Fix
You have a global variable with the same name and assigned to it inside a function without global declaration. Either add 'global x' inside the function or rename the local variable.
Symptom · 03
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Fix
One variable holds an unexpected type. Print type(variable_name) for all operands. Check if any variable was reassigned to a different type earlier in the flow.
Symptom · 04
AttributeError: 'int' object has no attribute 'append'
Fix
You assigned an integer to a variable originally intended to be a list. Check for unintended reassignment, e.g., my_list = some_function() that returned an int.
★ Quick Debug Cheat Sheet: Variable IssuesCommands to diagnose variable-related runtime errors fast
Variable not defined
Immediate action
Check the exact name and scope
Commands
print(dir())
print('variable_name' in locals() or 'variable_name' in globals())
Fix now
Assign a default value before usage or define the variable earlier
Variable has wrong type+
Immediate action
Print type of the variable
Commands
print(type(variable_name))
print(repr(variable_name))
Fix now
Convert with int(), str(), float() or fix the upstream assignment
Changed variable unexpectedly+
Immediate action
Find where it was last assigned
Commands
import pdb; pdb.set_trace()
!print(variable_name)
Fix now
Add watch or breakpoint at each assignment site
Python vs Java/C++ Variables
AspectPython VariablesVariables in Java / C++
Type declaration required?No — Python infers it automaticallyYes — you must write int age or String name
Can the type change after assignment?Yes — assign a new type freelyNo — the type is locked at declaration
Syntax to create a variableage = 25int age = 25;
Semicolon at end of line?Never neededRequired in Java/C++
Check variable type at runtime?type(variable_name)Not straightforwardly possible (limited reflection)
Swap two variables elegantly?a, b = b, a (one line)Requires a temporary third variable
Constant declaration?Convention: MAX_SIZE = 100 (no enforcement)final int MAX_SIZE = 100; (enforced)
Scope of name resolution?LEGB: Local, Enclosing, Global, Built-inBlock-level in Java; function-level in C++
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
variable_basics.pyplayer_name = "Alex" # Storing a piece of text (called a string)What a Variable Actually Is (And Why Python Makes Them Effor
variable_naming.pyuser_age = 28 # snake_case — the Python standardThe Rules and Best Practices for Naming Python Variables
advanced_assignment.pyfirst_name, last_name, age = "Maria", "Santos", 31Multiple Assignment, Swapping Values, and Checking Types
variable_scope.pyuser_count = 0 # This exists for the entire programVariable Scope
constants.pyMAX_RETRIES = 3Constants in Python – The Convention That Replaces the Keywo
TypeHintBreakdown.pyfrom __future__ import annotationsWhy Python Variables Can Suddenly Break (And How Type Hintin
IdentityCrisis.pysmall_a = 256The `is` Operator Isn't a Cooler `==`
MutationAmbush.pyoriginal_config = ["host", "port"]Variable Mutation Traps
name_binding.pya = [1, 2, 3]Python Name Binding Model
mutable_vs_immutable.pys = "hello"Mutable vs Immutable
type_annotations.pyname: str = "Alice"Type Annotations for Variables in Modern Python

Key takeaways

1
A Python variable is a named label pointing to a value in memory
not a fixed container. The label can be pointed at a completely different value (even a different type) at any time.
2
Python's single equals sign = means assignment (store this value), not equality. Use == when you want to compare two values.
3
Naming matters more than you think
player_score communicates intent instantly; p does not. Every variable name is a micro-comment about what the code does.
4
type() is your debugging best friend
when a variable isn't behaving as expected, call type() on it immediately. Nine times out of ten it's storing '42' (a string) instead of 42 (an integer).
5
Scope matters
variables defined inside functions are local by default. Use global only when absolutely necessary — it couples functions to external state.
6
Python constants are a convention, not a language feature. Name them in ALL_CAPS and never reassign
rely on code review for enforcement.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a variable and a value in Python? Can you...
Q02JUNIOR
Python is described as dynamically typed. What does that mean, and how d...
Q03SENIOR
What happens in memory when you reassign a Python variable — for example...
Q01 of 03JUNIOR

What is the difference between a variable and a value in Python? Can you give a concrete example?

ANSWER
A variable is a name that refers to a value. The value is the actual data stored in memory. For example, in x = 10, x is the variable (a reference), and 10 is the integer value. The variable points to the value, not the other way around. If you reassign x = 'hello', the name x now points to a string, but the integer 10 might still exist in memory (if referenced elsewhere) or be garbage collected.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Do I need to declare the type of a variable in Python?
02
Can a Python variable change its type after it's been created?
03
What is the difference between a local variable and a global variable in Python?
04
How do I create a constant in Python?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Python Basics. Mark it forged?

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

Previous
Python Data Types
4 / 17 · Python Basics
Next
Operators in Python