🧮 Variables

Python Variables — The Complete Guide

How Python actually stores your data behind the scenes, the rules and conventions for naming things well, and the scope rules that decide where a variable is visible and where it isn't.

📅

Last Updated

April 2026

⏱️

Read Time

39 min

🎯

Level

Beginner

What Is a Variable in Python?

A variable is a name that refers to a value stored in your computer's memory, so that value can be used, changed, or referenced again later in your program without having to retype it every time. Instead of writing the number 29 repeatedly throughout a program, you can store it once under a name like temperature, and from that point on the program can simply refer to temperature wherever that value is needed.

In Python, creating a variable is done through assignment, using the single equals sign =. This is one of the very first pieces of syntax most beginners encounter, and it looks deceptively simple, but it's worth understanding precisely what happens underneath it, because that understanding avoids a whole category of confusion later on.

🐍 Pythonfirst_variable.py
temperature = 29
city = "Patna"
is_raining = False

print(city, "is", temperature, "degrees today.")
print("Raining:", is_raining)

Unlike many other languages, Python doesn't require you to state in advance what type of value a variable will hold. There's no separate keyword like int, string, or var needed before the name — you simply write the name, an equals sign, and the value, and Python takes care of the rest. This absence of an explicit declaration step is one of the clearest, earliest signs of Python's broader design philosophy: reduce ceremony wherever it doesn't add genuine value.

It's worth being precise about the terminology here, because Python's own documentation is precise about it too. A variable in Python is more accurately described as a name bound to an object, rather than a labelled box that directly contains a value the way it's often taught in introductory courses for other languages. That distinction sounds subtle right now, but it becomes genuinely important once you start working with mutable data like lists, so it's worth carrying forward rather than dismissing as a technicality.

Names, Not Boxes — How Python Actually Stores Variables

Many beginner resources describe a variable as a labelled box that holds a value, and while that mental model is a reasonable starting point, it doesn't quite match how Python works internally, and the gap matters once your programs grow beyond the simplest examples. In Python, every value you create — a number, a string, a list, anything — exists in memory as an object. A variable name isn't a box containing that object; it's more like a label or a sticky note pointing at that object. Assignment doesn't copy a value into a box — it attaches a name to an already-existing object.

🐍 Pythonnames_point_to_objects.py
a = [1, 2, 3]
b = a  # b now points to the SAME list object as a, not a copy of it

b.append(4)

print(a)  # [1, 2, 3, 4]  -- changing b also changed what a sees
print(b)  # [1, 2, 3, 4]
print(a is b)  # True -- both names refer to the identical object in memory

This example often genuinely surprises beginners the first time they encounter it, precisely because the 'box' mental model would predict that changing b shouldn't affect a at all. But since both names are simply two separate labels pointing at the exact same list object in memory, a change made through either name is visible through the other, because there was only ever one object to begin with.

Compare that with a similar-looking example using an immutable type like an integer, and the behaviour looks different on the surface, even though the underlying rule — names point to objects — never actually changes.

🐍 Pythonimmutable_reassignment.py
x = 10
y = x  # y points to the same integer object 10 that x points to

y = y + 5  # this does NOT modify the object 10; it creates a brand new object, 15,
           # and rebinds the name y to point at that new object instead

print(x)  # 10 -- x still points at the original object
print(y)  # 15 -- y now points at a different object entirely

The difference between these two examples has nothing to do with variables behaving inconsistently — it comes down entirely to whether the underlying object itself is mutable (changeable in place, like a list) or immutable (unchangeable once created, like an integer or a string). Modifying a mutable object in place, as with append() above, is visible through every name pointing at that same object. Reassigning a name to a new value, as with y = y + 5, never modifies anything — it simply points that one name at a newly created object, leaving any other name (like x) untouched. This single idea — that assignment binds names to objects rather than copying values into boxes — quietly explains a large share of the 'unexpected' behaviour beginners run into with variables, especially once function arguments and lists enter the picture.

Dynamic Typing — Python Variables Don't Have a Fixed Type

Python is dynamically typed, which means a variable name is never permanently locked to a single data type. The same name can be reassigned to point at an integer at one moment, and a completely different type — a string, a list, even a function — moments later, without Python raising any complaint.

🐍 Pythondynamic_typing_demo.py
value = 42
print(type(value))  # <class 'int'>

value = "forty-two"
print(type(value))  # <class 'str'>

value = [4, 2]
print(type(value))  # <class 'list'>

It's the name that's flexible here, not the object itself — each of those reassignments creates a completely separate object of the new type and simply points the name value at whichever object is current. Nothing about the earlier integer object 42 or the string object "forty-two" changes retroactively; they simply stop being referenced by that particular name once it's reassigned, and Python's garbage collector eventually reclaims the memory they occupied if nothing else is still pointing at them.

This is in direct contrast to statically typed languages such as Java or C++, where a variable's type is fixed permanently at the moment it's declared, and attempting to later assign a value of a different, incompatible type to that same variable produces a compile-time error rather than being silently allowed. Dynamic typing is a large part of why Python code tends to look shorter and feel faster to write — there's no type declaration overhead at all — but it does shift a certain category of type-related bugs from being caught immediately during compilation to potentially surfacing only later, when the program is actually run with a particular set of inputs.

It's worth being precise about a related but distinct concept here too: Python is dynamically typed, but it is also strongly typed. Strong typing means Python won't silently and automatically convert between fundamentally incompatible types behind your back the way some dynamically typed languages do — attempting to add a string directly to an integer, for instance, raises a clear error rather than quietly producing an unexpected result.

🐍 Pythonstrong_typing_demo.py
age = 25
message = "I am " + age + " years old"
# TypeError: can only concatenate str (not "int") to str

# The correct, explicit approach:
message = "I am " + str(age) + " years old"
print(message)  # I am 25 years old

Variable Naming Rules — What Python Requires

Python enforces a specific, strict set of rules for what counts as a valid variable name, formally called an identifier. Breaking any of these rules produces an actual SyntaxError — these aren't stylistic suggestions, they're requirements the interpreter checks before your code can even run.

  • Must start with a letter or an underscore — a variable name cannot begin with a digit. names1 is valid, but 1name is not.

  • The remaining characters can be letters, digits, or underscores — after the first character, digits are allowed anywhere in the name, e.g. price_2026 is a valid identifier.

  • No spaces or special symbols — characters like spaces, hyphens, or punctuation marks (other than the underscore) are not permitted anywhere in a variable name.

  • Case-sensitive — Python treats age, Age, and AGE as three entirely distinct, unrelated names, since capitalisation is significant.

  • Cannot be a reserved keyword — words that already have a special meaning in the language, such as if, for, class, or return, cannot be used as variable names.

🐍 Pythonvalid_and_invalid_names.py
# Valid identifiers
user_name = "Rahul"
_temp = 98.6
score2 = 85
TotalAmount = 4599

# Invalid identifiers -- each of these raises a SyntaxError
# 2score = 85          -> cannot start with a digit
# user-name = "Rahul"  -> hyphens are not allowed
# class = "Physics"    -> 'class' is a reserved keyword
# total amount = 500   -> spaces are not allowed

The full list of reserved keywords that can never be used as a variable name can always be checked directly from within Python itself, by importing the built-in keyword module — a small but genuinely useful trick for confirming whether a particular word you had in mind is actually safe to use.

🐍 Pythonlist_all_keywords.py
import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
#  'break', 'class', 'continue', 'def', 'del', 'elif', 'else', ...]

print(keyword.iskeyword("class"))  # True
print(keyword.iskeyword("class_"))  # False -- adding an underscore sidesteps the conflict

Naming Conventions — Writing Names Python Developers Expect

Beyond the strict rules Python enforces, there's a separate layer of widely followed conventions — not enforced by the interpreter, but strongly expected by the community and formalised in PEP 8, Python's official style guide. Following these conventions isn't optional in any practical sense once you're writing code others will read, since deviating from them makes code noticeably harder for experienced Python developers to skim quickly.

ElementConventionExample
Ordinary variablessnake_case — all lowercase, words separated by underscorestotal_price, user_email, is_active
ConstantsUPPER_SNAKE_CASE — all uppercase, words separated by underscoresMAX_CONNECTIONS, PI, DEFAULT_TIMEOUT
Class namesPascalCase — each word capitalised, no underscoresShoppingCart, UserProfile
'Private' internal namesA single leading underscore, by convention_internal_cache, _helper_value
Names to avoid clashing with keywordsA single trailing underscoreclass_, type_, id_

Beyond formatting, choosing genuinely descriptive names matters just as much as following the correct casing style. A variable named d tells a reader nothing about its purpose, while a variable named days_since_last_login makes the surrounding code far easier to understand at a glance, without needing a comment to explain it. This is directly connected to the idea of self-documenting code — a well-chosen variable name does a meaningful share of the explanatory work a comment would otherwise have to do.

🐍 Pythonnaming_quality_comparison.py
# Poor: short, cryptic names that hide the actual meaning
n = 3
p = 499
t = n * p

# Better: descriptive names that make the logic self-explanatory
quantity = 3
unit_price = 499
total_cost = quantity * unit_price

A few smaller conventions round this out. Boolean variables are typically named to read naturally in a condition — is_valid, has_permission, can_edit — so that an if statement reads almost like a plain English sentence. Names representing collections are usually plural — orders, usernames — to visually distinguish them from a variable holding a single item, such as order or username.

Assignment Styles — Single, Multiple, and Chained

Beyond the ordinary single-variable assignment shown earlier, Python supports several additional assignment styles that make certain common patterns noticeably shorter and clearer to write.

🐍 Pythonmultiple_assignment.py
# Assigning different values to multiple variables in a single line
name, age, city = "Meera", 27, "Lucknow"

print(name)  # Meera
print(age)   # 27
print(city)  # Lucknow

# The number of variables on the left must match the number of values on the right,
# otherwise Python raises a ValueError
🐍 Pythonchained_assignment.py
# Assigning the same value to several variables at once
x = y = z = 0

print(x, y, z)  # 0 0 0

# Each name is bound independently to the same object;
# reassigning one does not affect the others
x = 5
print(x, y, z)  # 5 0 0

A particularly popular use of multiple assignment is the classic swap idiom, which lets you exchange the values held by two variables in a single line, without needing a separate temporary variable the way many other languages require.

🐍 Pythonswap_variables.py
a = 10
b = 25

a, b = b, a

print(a)  # 25
print(b)  # 10

Python also supports unpacking, a closely related feature that lets you assign the individual elements of a list or tuple directly to separate variable names in one step, including a special * syntax for capturing 'everything else' into a single list.

🐍 Pythonunpacking_demo.py
coordinates = (12.9, 77.6, "Bengaluru")
latitude, longitude, place = coordinates

print(latitude, longitude, place)  # 12.9 77.6 Bengaluru

scores = [88, 92, 79, 95, 60]
first, second, *rest = scores

print(first)   # 88
print(second)  # 92
print(rest)    # [79, 95, 60]

Augmented Assignment Operators

A very common pattern in programming is updating a variable based on its own current value — adding to a running total, for instance. Python provides shorthand augmented assignment operators that combine an arithmetic operation and an assignment into a single, more compact expression.

🐍 Pythonaugmented_assignment.py
total = 100

total += 20   # equivalent to: total = total + 20
print(total)  # 120

total -= 30   # equivalent to: total = total - 30
print(total)  # 90

total *= 2    # equivalent to: total = total * 2
print(total)  # 180

total //= 4   # equivalent to: total = total // 4
print(total)  # 45
OperatorEquivalent ToMeaning
+=x = x + yAdds and reassigns
-=x = x - ySubtracts and reassigns
*=x = x * yMultiplies and reassigns
/=x = x / yDivides (true division) and reassigns
//=x = x // yFloor-divides and reassigns
%=x = x % yApplies the modulus and reassigns
**=x = x ** yRaises to a power and reassigns

Beyond simply being shorter to type, augmented assignment carries a subtle but genuinely important behavioural difference for mutable types like lists: where possible, Python performs the operation in place on the existing object rather than creating a brand new one. For a list specifically, my_list += [4] extends the original list object directly, whereas my_list = my_list + [4] creates an entirely new list object and rebinds the name to it. In ordinary day-to-day code this distinction rarely matters, but it becomes relevant the moment other variables are also pointing at that same list, since only the in-place version is visible through those other names too.

Constants in Python — A Convention, Not a Language Feature

Many languages offer a dedicated keyword — const or final, for example — that genuinely prevents a variable from ever being reassigned once it's set. Python has no such built-in mechanism at the language level; technically, any variable in Python can always be reassigned at any point, no matter how it was originally intended to be used.

Instead, Python relies entirely on a naming convention to signal intent: a variable written in UPPER_SNAKE_CASE is understood by convention to be a constant — a value that is not meant to be changed after it's defined — even though Python itself does absolutely nothing to enforce that rule technically. It's purely a signal to other developers (and to your future self) reading the code.

🐍 Pythonconstants_convention.py
PI = 3.14159
MAX_LOGIN_ATTEMPTS = 5
DEFAULT_TIMEOUT_SECONDS = 30

def calculate_circle_area(radius):
    return PI * radius ** 2

# Nothing technically stops this next line from running,
# but doing it would violate the convention and confuse anyone reading the code:
# PI = 3.2

For situations where you genuinely need enforced immutability rather than just a naming signal, Python does offer some real tools, though they work differently from a simple const keyword. The typing.Final annotation, checked by static type-checking tools like mypy rather than at runtime, can flag an accidental reassignment before the code is even run. For grouping a fixed, related set of named constants together, Python's built-in enum module provides a proper, structured way to define a closed set of values, which is often a cleaner solution than a loose collection of separate uppercase variables.

Variable Scope — Where a Variable Is Actually Visible

Scope refers to the region of a program where a particular variable name can be accessed. Not every variable is visible everywhere in a program — a name created inside a function, for instance, generally doesn't exist outside of it. Understanding scope is essential for avoiding a whole category of confusing bugs, especially the moment functions start appearing in your code.

🐍 Pythonlocal_vs_global.py
message = "Hello from the global scope"  # a global variable

def greet():
    local_message = "Hello from inside the function"  # a local variable
    print(local_message)
    print(message)  # functions CAN read global variables

greet()
print(message)

# print(local_message)  # NameError: local_message only exists inside greet()

A variable created inside a function is called a local variable, and it only exists for as long as that function is running — it's created fresh each time the function is called, and it disappears entirely once the function finishes. A variable created outside of any function, at the top level of a script or module, is called a global variable, and it remains accessible from anywhere in that same file, including from inside functions, as the example above demonstrates by successfully printing message from inside greet().

It's important to notice the asymmetry here: functions can freely read a global variable without any special syntax, but by default, they cannot reassign a global variable from inside the function — attempting to do so simply creates a brand new local variable with the same name instead, silently leaving the original global variable untouched.

🐍 Pythonshadowing_pitfall.py
counter = 0

def increment():
    counter = counter + 1  # UnboundLocalError!
    print(counter)

increment()

# Python sees the assignment 'counter = ...' anywhere inside the function
# and decides counter is local to that function for its ENTIRE body --
# which means the read on the right-hand side happens before any local
# value has been assigned yet, raising an error.

This particular error trips up a huge number of beginners the first time they encounter it, precisely because it looks like it should work exactly like the earlier example that successfully read message. The crucial difference is that the earlier example only read the global variable, while this one attempts to reassign it — and the mere presence of an assignment to that name anywhere inside the function is enough for Python to treat it as local throughout the entire function body, even on lines that come before the assignment.

The global and nonlocal Keywords

When you genuinely do need to modify a global variable from inside a function, Python provides the explicit global keyword to state that intention clearly, rather than allowing it to happen silently or by accident.

🐍 Pythonglobal_keyword.py
counter = 0

def increment():
    global counter
    counter = counter + 1

increment()
increment()
print(counter)  # 2 -- the global variable was genuinely updated

There's a closely related but less commonly used keyword, nonlocal, which addresses a similar situation specifically for nested functions — a function defined inside another function. It lets an inner function modify a variable that belongs to its immediately enclosing function's scope, rather than the true global scope.

🐍 Pythonnonlocal_keyword.py
def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

Both global and nonlocal exist for the same underlying reason: by default, Python assumes an assignment inside a function creates a new local variable, purely as a safety measure to prevent functions from silently and accidentally overwriting variables that belong to a different, outer scope. These keywords are the deliberate, explicit way of opting out of that safety measure when it's genuinely needed, and using them sparingly — reaching for them only when truly necessary — is widely considered better practice than relying on global state throughout a program, since functions that freely modify variables outside themselves tend to become harder to reason about and test as a codebase grows.

The LEGB Rule — How Python Resolves a Variable's Scope

When Python encounters a variable name being read anywhere in your code, it needs a consistent rule for deciding which specific variable — among potentially several with the same name in different scopes — it should actually use. This resolution order is commonly summarised by the acronym LEGB, standing for Local, Enclosing, Global, and Built-in, checked strictly in that order.

ScopeWhat It CoversExample
Local (L)Names created inside the current functionA variable defined directly inside the function currently running
Enclosing (E)Names in any enclosing function, for nested functionsA variable from an outer function, accessed by an inner nested function
Global (G)Names defined at the top level of the current module/fileA variable created outside any function, at the top of the script
Built-in (B)Names Python provides automatically, with no import neededFunctions like print, len, or range, and types like list, str, int
🐍 Pythonlegb_demo.py
# Global scope
value = "global value"

def outer():
    # Enclosing scope (relative to inner())
    value = "enclosing value"

    def inner():
        # Local scope
        value = "local value"
        print(value)  # local value -- Local wins first

    inner()
    print(value)  # enclosing value

outer()
print(value)  # global value
print(len)     # built-in function, found only because nothing local shadows it

Python checks these four scopes strictly in Local, then Enclosing, then Global, then Built-in order, stopping at the very first match it finds. This is exactly why defining a local variable with the same name as a global one — or, more subtly, the same name as a built-in function like list or str — silently 'hides' that outer name for the rest of the current scope, a phenomenon commonly called shadowing. Accidentally naming a variable list is a classic beginner trap for exactly this reason: it doesn't raise any error immediately, but it quietly makes the real, built-in list type inaccessible by that name for the remainder of that scope, which can produce a confusing error later on a completely different line.

Type Hints — Optional, Tool-Checked Type Documentation

Even though Python is dynamically typed and doesn't require declaring a variable's type up front, modern Python supports an optional feature called type hints, introduced primarily through PEP 484. A type hint lets you annotate what type a variable is expected to hold, purely as extra information for humans and external tools — it has no effect on how the program actually runs, and Python's own interpreter does not enforce it at runtime.

🐍 Pythontype_hints_demo.py
age: int = 25
name: str = "Karan"
price: float = 499.99
is_available: bool = True

# Type hints are purely advisory at runtime -- Python will NOT stop this:
age = "twenty-five"  # runs without error, even though it contradicts the hint above
print(age)

The genuine value of type hints comes from external static type-checking tools, most notably mypy, which read these annotations and flag a mismatch — such as the reassignment shown above — as an error before the code is ever run, without needing to actually execute the program to catch the problem. Many modern code editors also use type hints to power more accurate autocomplete suggestions, since the editor can reliably tell what methods and attributes a given variable is expected to support.

Type hints are entirely optional, and an enormous amount of perfectly good Python code, especially in smaller scripts, doesn't use them at all. They tend to earn their keep specifically in larger codebases maintained by multiple people over a long period, where the extra clarity and tooling support meaningfully reduce a certain category of bugs that would otherwise only surface at runtime, sometimes long after the code was originally written.

None — Representing 'No Value Yet'

Python has no concept of a variable existing but being 'empty' or 'uninitialised' the way some languages do — a name in Python either currently points at some object, or it doesn't exist at all yet in that scope, in which case referencing it raises a NameError. What Python does provide is a special, singleton value called None, used specifically to represent the deliberate absence of a meaningful value.

🐍 Pythonnone_demo.py
middle_name = None  # deliberately indicates 'not provided', not simply forgotten

def find_user(user_id):
    # imagine this searches a database and doesn't find a match
    return None

result = find_user(999)

if result is None:
    print("No user found with that ID.")
else:
    print("Found:", result)

Notice the comparison style used above: checking for None is conventionally done with is None rather than == None, and this isn't just a stylistic preference. Since None is a genuine singleton object — there is exactly one None in an entire running Python program — the identity check is is both the technically correct tool for the job and, in practice, marginally faster, since it doesn't need to invoke any custom equality comparison logic that a more unusual object might define.

Everything Is an Object — Even Variables Themselves Reflect This

One of Python's more conceptually important design decisions is that genuinely everything is an object — not just the values you'd intuitively expect, like numbers, strings, and lists, but also functions, classes, and even modules. This uniformity has direct, practical consequences for how variables behave, and it's worth seeing concretely rather than only as an abstract claim.

🐍 Pythoneverything_is_an_object.py
def greet():
    print("Hello!")

# A function can be assigned to a variable, just like any other value
say_hello = greet
say_hello()  # Hello!

print(type(42))       # <class 'int'>
print(type("hi"))     # <class 'str'>
print(type(greet))    # <class 'function'>
print(type(print))    # <class 'builtin_function_or_method'>

Because functions are objects just like numbers and strings, a variable can point at a function exactly the same way it points at any other value — there's no special, separate syntax needed. This is precisely what makes it possible to pass a function as an argument to another function, or store a collection of functions inside a list, patterns that show up constantly in idiomatic Python code, particularly once you move into more functional-style programming or working with callbacks.

Identity vs Equality — is vs ==

Because a variable in Python is a name pointing at an object rather than a box containing a copy of a value, Python offers two genuinely different ways to compare variables, and confusing the two is a common source of subtle bugs. The == operator checks whether two variables hold values that are considered equal, based on the type's own definition of equality. The is operator checks something stricter: whether two variables point at the exact same object in memory — their identity.

🐍 Pythonidentity_vs_equality.py
list_a = [1, 2, 3]
list_b = [1, 2, 3]
list_c = list_a

print(list_a == list_b)  # True -- same contents, so considered equal
print(list_a is list_b)  # False -- two separate, distinct list objects

print(list_a == list_c)  # True -- same contents
print(list_a is list_c)  # True -- literally the same object, since list_c = list_a

In everyday code, == is almost always the comparison you actually want — you generally care whether two variables represent the same value, not whether they happen to be the identical object in memory. The main legitimate use of is is specifically for comparing against Python's singleton values, most commonly None, and occasionally True or False, precisely because those particular values are guaranteed to exist as one single shared object throughout a running program, making an identity check both meaningful and reliable in that specific case.

Common Mistakes Beginners Make With Variables

A handful of specific mistakes involving variables come up so consistently among beginners that they're worth calling out explicitly, since recognising them in advance often prevents a fair amount of confused debugging later.

  • Confusing = and == — a single equals sign assigns a value to a variable, while a double equals sign checks for equality between two values. Writing if x = 5: instead of if x == 5: raises a SyntaxError, but the confusion still trips up many newcomers switching from other contexts, like spreadsheet formulas.

  • Assuming a variable copies a mutable value — as shown earlier, assigning one variable to another (b = a) for a mutable object like a list does not create an independent copy; both names point at the same underlying object, and this frequently surprises beginners the first time they modify one and see the other change too.

  • Shadowing built-in names — naming a variable list, str, id, or type overrides Python's own built-in functions or types with that same name for the rest of that scope, which can cause a confusing, seemingly unrelated error much later in the same block of code.

  • Reassigning a global variable inside a function without the global keyword — this doesn't raise an error the way many beginners expect; it silently creates a separate local variable instead, leaving the original global variable completely unchanged.

  • Using vague, single-letter variable names outside of very short, obvious contexts — names like x, y, or tmp used throughout a longer function make the code considerably harder for anyone (including your future self) to follow without re-reading it carefully.

  • Forgetting that variable names are case-sensitive — accidentally typing Total instead of total (or vice versa) creates an entirely new, separate variable rather than referencing the one already intended, often producing a NameError rather than the expected value.

🐍 Pythonmutable_default_pitfall.py
# A particularly well-known, more advanced pitfall involving variables and mutability:
def add_item(item, cart=[]):  # a mutable default argument, created only ONCE
    cart.append(item)
    return cart

print(add_item("apple"))    # ['apple']
print(add_item("banana"))   # ['apple', 'banana'] -- unexpectedly carries over!

# The fix: use None as the default, and create a fresh list inside the function
def add_item_fixed(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

Variables Across Different Contexts

The basic rules covered so far apply consistently everywhere, but variables show up in a few additional contexts worth knowing about as you continue learning, since each has its own small nuance.

🐍 Pythonloop_and_comprehension_variables.py
# A loop variable is an ordinary variable, and it remains accessible after the loop ends
for number in range(3):
    pass
print(number)  # 2 -- the loop variable still holds its final value

# A comprehension variable, however, has its OWN separate, private scope
squares = [n * n for n in range(5)]
print(squares)  # [0, 1, 4, 9, 16]
# print(n)  # NameError -- 'n' does not leak out of the comprehension

This distinction is a common surprise: an ordinary for loop's variable behaves like any other local (or global) variable and remains accessible after the loop finishes, while a variable used inside a list, set, or dictionary comprehension is deliberately scoped only to that comprehension itself, and cannot be accessed outside of it. This was a specific, intentional design change introduced in Python 3, aimed at preventing comprehension variables from accidentally leaking into and overwriting a variable of the same name in the surrounding scope.

Variables used as function parameters are, by default, local to that function, following exactly the same local-scope rules already covered. Class attributes and instance attributes — variables that belong to an object created from a class — follow a related but distinct set of rules involving self, which is generally covered in detail as part of learning object-oriented programming specifically, rather than as a core part of basic variable usage.

Checking and Inspecting Variables

Python provides several handy built-in tools for inspecting a variable while you're writing or debugging code, all of which are worth knowing early since they come up constantly during everyday development.

🐍 Pythoninspecting_variables.py
quantity = 5

print(type(quantity))        # <class 'int'> -- shows the variable's current type
print(id(quantity))          # a large integer -- the object's unique memory identity
print(isinstance(quantity, int))  # True -- checks if a variable IS a given type

# dir() lists everything available on an object, useful for exploring what a value can do
# print(dir(quantity))

# globals() and locals() return dictionaries of currently defined variables
print("quantity" in globals())  # True, if run at the top level of a script

type() is by far the most commonly used of these during everyday development, since confirming exactly what type a variable currently holds is often the fastest way to diagnose an unexpected error. isinstance() is generally the preferred way to actually check a variable's type inside real program logic (for example, inside an if statement), since, unlike a direct comparison using type(), it correctly accounts for inheritance relationships between classes.

Interview Questions on Python Variables

Practice Questions — Test Your Understanding

1. Create three variables — a string, an integer, and a boolean — and print all three on a single line.

Easy

2. Which of the following are valid Python variable names, and why: 2total, _score, my-value, class, user_1?

Easy

3. Write a single line of code that swaps the values of two variables, a and b, without using a third temporary variable.

Medium

4. Explain what will be printed by the following code, and why: x = [1, 2]\ny = x\ny.append(3)\nprint(x)

Medium

5. Write a function that increments a global variable called total_visits by 1 each time it is called, using the correct keyword to do so.

Medium

6. What is the output of the following, and why: print([n for n in range(3)])\nprint(n)?

Hard

7. Why is defining a function with a mutable default argument, like def add(item, cart=[]):, considered a common pitfall, and how would you fix it?

Hard

8. Two variables, p and q, both refer to lists containing the same elements: [1, 2, 3], but were created as two entirely separate list literals. What will p == q and p is q each evaluate to, and why?

Hard

Conclusion — Building on a Solid Foundation

Variables might be the very first concept covered in any Python course, but as this guide has hopefully shown, there's genuine depth underneath what looks like a simple = sign. Understanding that a variable is a name bound to an object — rather than a box containing a value — quietly explains a surprising share of behaviour that otherwise looks inconsistent or confusing, particularly once mutable data and functions enter the picture.

The practical habits worth carrying forward from here are straightforward: choose descriptive, convention-following names, understand the difference between local and global scope before it causes a confusing bug, be deliberate about when a variable should hold a mutable versus an immutable type, and reach for tools like type hints once your projects grow large enough to benefit from them. With that foundation in place, the rest of the language — data types, control flow, functions, and eventually classes — builds directly and naturally on top of it.

Frequently Asked Questions (FAQ)