Variables & Data Types
Topic 1 of 8, with 4 concept checks. Names, values, and Python's built-in types
Trace names, values, and runtime types
Names and objects
Follow what each name refers to, how rebinding changes that reference, and where Python discovers a value's type at runtime before you reason about larger expressions.
Core lesson 01
Mutable objects change in place (id() stays the same). Immutable objects can't — any 'change' creates a brand new object.
Every value in Python is an object with an identity, type, and value. Mutability is about whether that value can change after creation. Mutable types (list, dict, set) can be edited in place — the object's id() stays constant. Immutable types (int, float, str, tuple, bool, frozenset) can never be altered; operations that look like edits actually build and return a new object.
a = [1, 2, 3]
print(id(a))
a.append(4)
print(id(a)) # same id -> mutated in place
s = "hi"
print(id(s))
s = s + "!"
print(id(s)) # different id -> new objectWhat to remember
What's the difference between mutable and immutable types in Python? Name two of each.
Common footguns
- Assuming `tuple` is 'fully' immutable — a tuple containing a list still lets you mutate that inner list.
- Passing a mutable object into a function and being surprised the caller's copy changed too — Python passes references, not copies.
IMMUTABLE (int, str, tuple) MUTABLE (list, dict, set) x = 5 id: 0x1001 a = [1,2] id: 0x2001 x = x+1 a.append(3) x -> id: 0x1002 (NEW object) a -> id: 0x2001 (SAME object)
Core lesson 02
No — 1 + '1' raises a TypeError. Python coerces compatible numeric types, but never silently converts between unrelated types.
'Dynamically typed' means variables aren't declared with a fixed type. 'Strongly typed' means Python still refuses to operate on incompatible types without an explicit conversion. int + float works because both are numeric and Python defines a coercion path between them. int + str has no defined coercion, so Python raises rather than guessing what you meant.
print(1 + 2.0) # 3.0 (int/float coercion is fine)
try:
print(1 + "1")
except TypeError as e:
print("TypeError:", e)
print(str(1) + "1") # "11" once you're explicitWhat to remember
Does Python automatically coerce types for 1 + '1'?
Common footguns
- Relying on implicit conversion in other languages (JS's `1 + '1' === '11'`) and expecting the same here.
- Forgetting that `input()` always returns a str — `input() + 1` fails until you wrap it in `int(...)`.
Core lesson 03
== compares values (calls __eq__). is compares identity — are these literally the same object in memory.
Every object has a unique id() for its lifetime. `is` checks whether two names refer to that same id. `==` instead calls the object's `__eq__` method to compare value equality, which you can define however makes sense for a custom class. Two different objects can be `==` equal while being distinct in memory (`is` False).
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(b is a) # True -- same object
print(c is a) # False -- different object
print(c == a) # True -- same valueWhat to remember
What's the difference between is and ==?
Common footguns
- Using `x == None` instead of `x is None` — works most of the time but isn't idiomatic and can misbehave with custom __eq__.
- Small integers (-5 to 256) and short strings are cached by CPython, so `is` can accidentally return True for them — don't rely on this.
a = [1,2,3] +------+ +---------+
b = a | a ---------->| [1,2,3] |<--- b also points here
b is a -> True +------+ +---------+ id: 0x3001
c = [1,2,3] +------+ +---------+
| c ---------->| [1,2,3] | id: 0x4002 (different box)
c == a -> True (same values)
c is a -> False (different boxes)Core lesson 04
type(1/2) is float (0.5). type(1//2) is int (0). / always true-divides; // floors the result.
Python 2 made `/` behave like floor division for two ints, which caused endless bugs. Python 3 fixed this: `/` is always true division and returns a float. `//` is floor division — it rounds toward negative infinity and returns an int if both operands are ints, or a float if either operand is a float.
print(1 / 2) # 0.5
print(1 // 2) # 0
print(7 // 2) # 3
print(-7 // 2) # -4 (floors toward -inf, not toward 0)
print(7.0 // 2) # 3.0 (float in -> float out)What to remember
What is type(1/2) vs type(1//2) in Python 3?
Common footguns
- Expecting `//` to truncate toward zero like C — it floors instead, so negative results can surprise you.
- Porting Python 2 code where `/` meant integer division and silently getting floats now.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Use descriptive snake_case names for variables (PEP 8).
- Avoid shadowing Python built-ins (list, dict, str, type) as variable names.
- Add type hints for clarity in larger codebases: `age: int = 25`.
- Prefer `is None` / `is not None` over `== None`.
Apply the concept in Interview practice
Two SumeasyLeetCode #1 · O(n) time, O(n) space
Store value→index in a dict while iterating once. For each number, check whether target−num is already a key.
Open problemFizzBuzzeasyHackerRank · O(n) time, O(1) space
Loop 1..n. If divisible by 15 print FizzBuzz, by 3 print Fizz, by 5 print Buzz, otherwise print the number.
Open problemMaking AnagramseasyHackerRank · O(n) time, O(k) space
Use collections.Counter on both strings; the answer is the total count of characters that don't overlap between the two counters.
Open problemReverse IntegermediumLeetCode #7 · O(log n) time, O(1) space
Pop digits with % 10 and // 10, rebuild the reversed number, and check for 32-bit signed overflow before returning — no string conversion needed.
Open problemPalindrome NumbereasyLeetCode #9 · O(log n) time, O(1) space
Without converting to a string, reverse the second half of the number mathematically and compare it to the first half.
Open problemConcept checks
What's the difference between mutable and immutable types in Python? Name two of each.
Hint
Think about whether you can change contents in place without creating a new object.
Try id(x) before and after modifying x — does it stay the same?
Answer
Mutable objects change in place (id() stays the same). Immutable objects can't — any 'change' creates a brand new object.
Every value in Python is an object with an identity, type, and value. Mutability is about whether that value can change after creation. Mutable types (list, dict, set) can be edited in place — the object's id() stays constant. Immutable types (int, float, str, tuple, bool, frozenset) can never be altered; operations that look like edits actually build and return a new object.
a = [1, 2, 3]
print(id(a))
a.append(4)
print(id(a)) # same id -> mutated in place
s = "hi"
print(id(s))
s = s + "!"
print(id(s)) # different id -> new objectIMMUTABLE (int, str, tuple) MUTABLE (list, dict, set) x = 5 id: 0x1001 a = [1,2] id: 0x2001 x = x+1 a.append(3) x -> id: 0x1002 (NEW object) a -> id: 0x2001 (SAME object)
Watch out
- Assuming `tuple` is 'fully' immutable — a tuple containing a list still lets you mutate that inner list.
- Passing a mutable object into a function and being surprised the caller's copy changed too — Python passes references, not copies.
Does Python automatically coerce types for 1 + '1'?
Hint
Try imagining what 1 + '1' would even mean.
Python is dynamically typed, but strongly typed — that's the key distinction here.
Answer
No — 1 + '1' raises a TypeError. Python coerces compatible numeric types, but never silently converts between unrelated types.
'Dynamically typed' means variables aren't declared with a fixed type. 'Strongly typed' means Python still refuses to operate on incompatible types without an explicit conversion. int + float works because both are numeric and Python defines a coercion path between them. int + str has no defined coercion, so Python raises rather than guessing what you meant.
print(1 + 2.0) # 3.0 (int/float coercion is fine)
try:
print(1 + "1")
except TypeError as e:
print("TypeError:", e)
print(str(1) + "1") # "11" once you're explicitWatch out
- Relying on implicit conversion in other languages (JS's `1 + '1' === '11'`) and expecting the same here.
- Forgetting that `input()` always returns a str — `input() + 1` fails until you wrap it in `int(...)`.
What's the difference between is and ==?
Hint
One compares values, one compares something else entirely.
Think identity (same object in memory) vs equality (same value).
Answer
== compares values (calls __eq__). is compares identity — are these literally the same object in memory.
Every object has a unique id() for its lifetime. `is` checks whether two names refer to that same id. `==` instead calls the object's `__eq__` method to compare value equality, which you can define however makes sense for a custom class. Two different objects can be `==` equal while being distinct in memory (`is` False).
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(b is a) # True -- same object
print(c is a) # False -- different object
print(c == a) # True -- same valuea = [1,2,3] +------+ +---------+
b = a | a ---------->| [1,2,3] |<--- b also points here
b is a -> True +------+ +---------+ id: 0x3001
c = [1,2,3] +------+ +---------+
| c ---------->| [1,2,3] | id: 0x4002 (different box)
c == a -> True (same values)
c is a -> False (different boxes)Watch out
- Using `x == None` instead of `x is None` — works most of the time but isn't idiomatic and can misbehave with custom __eq__.
- Small integers (-5 to 256) and short strings are cached by CPython, so `is` can accidentally return True for them — don't rely on this.
What is type(1/2) vs type(1//2) in Python 3?
Hint
Python 3 changed division behavior from Python 2 — there are now two division operators.
One is 'true division', the other is 'floor division'.
Answer
type(1/2) is float (0.5). type(1//2) is int (0). / always true-divides; // floors the result.
Python 2 made `/` behave like floor division for two ints, which caused endless bugs. Python 3 fixed this: `/` is always true division and returns a float. `//` is floor division — it rounds toward negative infinity and returns an int if both operands are ints, or a float if either operand is a float.
print(1 / 2) # 0.5
print(1 // 2) # 0
print(7 // 2) # 3
print(-7 // 2) # -4 (floors toward -inf, not toward 0)
print(7.0 // 2) # 3.0 (float in -> float out)Watch out
- Expecting `//` to truncate toward zero like C — it floors instead, so negative results can surprise you.
- Porting Python 2 code where `/` meant integer division and silently getting floats now.