2.1 Dynamic Typing
In a statically typed language like Java or C, you must announce a variable's type before you use it — int x = 5; tells the compiler, permanently, "x will always hold a whole number."
Python takes the opposite approach. You simply write x = 5, and Python looks at the value on the right-hand side and infers the type automatically — no announcement needed. This is called dynamic typing, and it goes one step further than most beginners expect:
The same variable name can be reassigned to a completely different type later in the program. x = 5 followed later by x = "hello" is perfectly legal Python — the name x simply now refers to a string instead of a number.
# Dynamic Typing in Action
x = 5 # x refers to an int object
print(type(x)) # <class 'int'>
x = "ReadyToGo" # x now refers to a string object
print(type(x)) # <class 'str'>
x = [1, 2, 3] # x now refers to a list object
print(type(x)) # <class 'list'>
Dynamic typing makes Python fast to write and flexible to iterate on, but it also means a type-related mistake (like accidentally adding a number to a string: 5 + "10") is only caught when that exact line actually runs, not ahead of time the way a compiler would catch it.
Type Hints & Static Analysis (PEP 484)
In modern Python (3.5+), you can add optional type hints to make your intent crystal clear to IDEs and tools like mypy, without losing runtime dynamic flexibility:
def calculate_total(price: float, tax_rate: float) -> float:
return price * (1.0 + tax_rate)
Python does not enforce these types at runtime, but static analysis tools will flag potential mismatches before code is deployed.
2.2 Built-in Data Types: Mutable vs Immutable
Every one of Python's built-in types falls into one of two camps, and understanding which camp a type belongs to explains a huge amount of Python's behavior later on.
1. Immutable Types
Immutable types — int, float, bool, str, and tuple — can never be changed after they are created. When code appears to "modify" a string or a number, what's actually happening behind the scenes is that Python builds a brand-new object and simply points your variable at it instead.
2. Mutable Types
Mutable types — list, dict, and set — are different: their contents really can be changed, grown, or shrunk in place, without creating a new object at all.
Why Immutability Matters in Architecture
- Hashability & Dictionary Keys: In Python, only immutable objects are hashable (they implement
__hash__). Because an immutable object's content can never change, its hash value remains constant for its entire lifetime. This makesint,str, andtuplesafe to use as dictionary keys or set elements, whilelistanddictwill raiseTypeError: unhashable type: 'list'. - Thread Safety: Immutable objects can be safely shared across multiple threads without locks or data race conditions because no thread can mutate their state.
2.3 Variables as References (Name Bindings)
This is the single most important mental model shift when moving from C-like languages into Python. In C, a variable is a labeled box: int x = 10; physically reserves a slot in memory and puts the value 10 directly inside it.
In Python, a variable is not a box — it's a name tag pointing at an object that lives somewhere else in memory. Writing x = 10 creates an integer object with the value 10, and then attaches the name x to it, like tying a label onto a balloon.
The Shared Reference Aliasing Trap
Now consider y = x. This does not copy the value into a new box called y — it ties a second label onto the exact same balloon. Both x and y now refer to the identical object in memory.
Why This Distinct Model Produces Classic Beginner Bugs
For immutable objects like integers this distinction rarely causes visible problems, because you can't change the object anyway. But for a mutable object — say, a list — this is exactly why a classic beginner bug appears: modifying the list through the name y is instantly visible when you look at it through the name x too, because there was only ever one list, with two labels on it:
# Classic Mutable Aliasing Bug
list_a = [10, 20, 30]
list_b = list_a # Ties label list_b onto the SAME list object
list_b.append(40) # Modifies the single shared object in-place
print(list_a) # Output: [10, 20, 30, 40] — list_a changed too!
print(list_a is list_b) # Output: True (Identical memory address)
# How to make an actual independent copy:
list_c = list_a.copy() # or list_a[:]
list_c.append(99)
print(list_a) # [10, 20, 30, 40] — protected from mutation!
2.4 Type Casting & Operators
Because Python infers types automatically, you sometimes need to explicitly convert — or cast — a value from one type to another. Python provides simple built-in functions for this:
int("5")turns the text"5"into the number5.float("3.14")turns text into a decimal number3.14.str(42)turns a number into text"42".list("abc")turns a string into a list of its individual characters['a', 'b', 'c'].
Operators in Python
Python supports the operators you'd expect from any language: arithmetic (+ - * / % ** //), comparison (== != > < >= <=), and logical operators — though logical operators are spelled out as English words (and, or, not) rather than symbols like &&, ||, !.
Two Special Operators to Highlight
Two operators are worth calling out specifically because C and Java don't have direct equivalents:
//performs floor division: division that rounds down to the nearest whole number.**performs exponentiation: raises the base to the power of the exponent.
# ARITHMETIC OPERATORS
x = 7
print(x / 2) # 3.5 (true division — always returns a float!)
print(x // 2) # 3 (floor division — rounds down to nearest whole number)
print(x ** 2) # 49 (exponentiation, x squared)
💡 Placement & Interview Deep-Dive
- Negative Floor Division: What is
-7 // 2? Beginners often assume-3. In Python, floor division rounds down toward negative infinity, so-7 // 2evaluates to-4! - Arbitrary-Precision Integers: In Python 3, integers have unbounded precision! You can calculate
2 ** 1000without any integer overflow error, unlike Java or C where integers wrap around at 32 or 64 bits. - Short-Circuit Evaluation of
and/or: In Python, logical operators return the value of the operand that determined the outcome, not just a raw boolean:print("" or "Default") # Output: "Default" print("Ready" and "Go") # Output: "Go"