Python · lesson 3 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python Variables and Type System
See how Python variables work by running them: names as references, mutable vs immutable objects, dynamic typing, PEP 8 names and LEGB scope.
By SkillByExample · Updated
Variables in Python are references to objects in memory. Unlike languages like C++ or Java, Python does not require explicit type declarations. The type of a variable is determined at runtime based on the object it references. Output below is from Python 3.13.
Variables as References
Assignment never copies an object; it makes a name refer to one. Two names can therefore refer to the same object.
x = 42
y = x
print(x is y)
x = x + 1
print(x, y)
a = [1, 2, 3]
b = a
b.append(4)
print(a, a is b)
c = a.copy()
c.append(5)
print(a, c)
$ python references.py
True
43 42
[1, 2, 3, 4] True
[1, 2, 3, 4] [1, 2, 3, 4, 5]
y = x made both names refer to the same 42 — is checks exactly that. x = x + 1 didn’t change that object (ints are immutable); it built a new 43 and moved the name x to it, so y still says 42. Lists are mutable: b.append(4) changed the one list both names share, so the change shows through a. copy() makes a separate list when you want independence.
Dynamic typing
A name has no type of its own — the object it refers to does — so the same name can refer to objects of different types over time.
value = 42
print(type(value).__name__)
value = "forty-two"
print(type(value).__name__)
value = [4, 2]
print(type(value).__name__, len(value))
try:
print("answer: " + 42)
except TypeError as e:
print("TypeError:", e)
$ python dynamic.py
int
str
list 2
TypeError: can only concatenate str (not "int") to str
Rebinding value to a string or a list is allowed. But Python is strongly typed: it won’t silently turn 42 into text — you have to convert it with str(42) or an f-string. Type hints (see type hints) let a checker catch such mismatches before the code runs.
Naming Conventions (PEP 8)
PEP 8, the standard style guide, uses snake_case for variables and functions and UPPER_SNAKE_CASE for constants. The real trap is reusing a built-in’s name:
user_name = "Alice"
MAX_RETRY_COUNT = 3
list = [1, 2, 3] # shadows the built-in list type
try:
letters = list("abc")
except TypeError as e:
print("TypeError:", e)
del list # remove the shadowing name
print(list("abc"))
$ python shadowing.py
TypeError: 'list' object is not callable
['a', 'b', 'c']
After list = [1, 2, 3], the name list found your list before the built-in type, so calling it failed — often far from the line that caused it. Deleting the global name let lookup fall through to the built-in again. Avoid names like list, dict, str, id and input for your variables.
Scope resolution (LEGB)
Python looks up a name in four scopes in order: Local (the current function), Enclosing (outer functions), Global (the module) and Built-in.
prefix = "User: " # global
def greet(name):
message = f"Hello, {name}" # local to greet
def format_output():
return f"{prefix}{message}" # message: enclosing, prefix: global
return format_output()
print(greet("Alice"))
print(len(prefix)) # len: built-in
try:
print(message)
except NameError as e:
print("NameError:", e)
$ python scope.py
User: Hello, Alice
6
NameError: name 'message' is not defined
format_output has no variables of its own, yet it found message one level out and prefix at module level. Outside greet, message doesn’t exist — local names live only while the function runs.
global and nonlocal
Reading an outer name needs nothing special, but assigning to it inside a function needs global (module level) or nonlocal (enclosing function).
count = 0
def increment():
global count
count += 1
increment()
increment()
print(count)
def make_counter():
total = 0
def step():
nonlocal total
total += 1
return total
return step
tick = make_counter()
print(tick(), tick(), tick())
$ python global_nonlocal.py
2
1 2 3
Without the declarations, count += 1 and total += 1 would each create a new local variable — and fail, as Practice 2 shows. Prefer nonlocal closures or return values over global; module-level state shared by many functions is hard to test.
Practice
1. Predict whether two names bound to the same short string literal refer to the same object, then check with is. Is that something to rely on?
Solution
a = "hello"
b = "hello"
print(a is b, a == b)
c = "".join(["hel", "lo"])
print(c is a, c == a)
$ python task1.py
True True
False True
CPython reuses identifier-like string literals, so a is b happens to be true; a string built at runtime is a different object with equal contents. That’s an implementation detail — compare strings with ==.
2. Remove the global count line from global_nonlocal.py and call increment(). What error appears, and why?
Solution
count = 0
def increment():
count += 1 # no global statement
increment()
$ python task2.py
Traceback (most recent call last):
File "/home/user/py-demo/variables/task2.py", line 6, in <module>
increment()
~~~~~~~~~^^
File "/home/user/py-demo/variables/task2.py", line 4, in increment
count += 1 # no global statement
^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Because increment assigns to count, Python treats count as local for the whole function. count += 1 then reads that local before it has a value. global count tells Python the name belongs to the module scope.
3. Write add_tag(tags) that appends "new" to the list it receives, call it with a list, and print the list afterwards. Then make a version that leaves the caller’s list untouched.
Solution
def add_tag(tags):
tags.append("new")
return tags
def add_tag_copy(tags):
return tags + ["new"]
mine = ["a"]
add_tag(mine)
print(mine)
theirs = ["b"]
result = add_tag_copy(theirs)
print(theirs, result)
$ python task3.py
['a', 'new']
['b'] ['b', 'new']
The function received a reference to the caller’s list, so append changed it. tags + ["new"] builds a new list and leaves the argument alone.