02 — Variables & Data Types
Q1. Which statement correctly describes Python's object model?
x = 42
def greet(): pass
- Only class instances are objects; primitives like
intand functions are special language constructs - Everything referenced by a name — integers, strings, functions, classes, and modules — is an object with a type and identity
- Only mutable values (lists, dicts) are objects; immutable values are stored inline, not as objects
- Objects exist only after you explicitly instantiate a class with
()
Show Answer
Answer: B — Everything referenced by a name — integers, strings, functions, classes, and modules — is an object with a type and identity
Explanation: In Python, "everything is an object" is not a slogan but a literal implementation fact: 42, greet, and even the int and function types themselves are objects with a type, an identity (id()), and typically attributes. Option A is the kind of assumption carried over from languages with true primitive types, which Python does not have. Option C is backwards — immutability affects whether an object can change in place, not whether it "is" an object. Option D is wrong because literals like 42 and "hi" are objects the moment they're created, with no explicit constructor call required.
Q2. What happens when a name already bound to an int is reassigned to a str?
status = 200
status = "OK"
- A
TypeError, because a variable's type cannot change after first assignment - This is legal — Python is dynamically typed, so a name can be rebound to any object regardless of the previous object's type
- The assignment silently converts
"OK"to an integer to match the variable's original type - It works only if both statements are inside the same
withblock
Show Answer
Answer: B — This is legal — Python is dynamically typed, so a name can be rebound to any object regardless of the previous object's type
Explanation: Python variables are just names in a namespace bound to objects; the name status has no fixed type of its own, so rebinding it to a str after an int is completely legal at runtime. Option A confuses Python with statically-typed languages that give variables a fixed declared type. Option C invents a coercion Python never performs implicitly between unrelated types. Option D's premise is irrelevant — no such scoping restriction exists.
Q3. What is the core difference between is and ==?
a = [1, 2, 3]
b = [1, 2, 3]
-
ischecks value equality;==checks object identity -
ischecks object identity (same object in memory);==checks value equality (calls__eq__) - They are fully interchangeable for all built-in types
-
isonly works on numbers;==only works on strings
Show Answer
Answer: B — is checks object identity (same object in memory); == checks value equality (calls __eq__)
Explanation: is answers "are these the same object?" (equivalent to comparing id(a) == id(b)), while == answers "do these compare equal?" by invoking __eq__. In the code above, a == b is True (same contents) but a is b is False (two distinct list objects). Option A states the definitions backwards. Option C is a common trap — for mutable objects and most non-cached immutables, is and == diverge exactly as shown here. Option D is simply false; both operators work across types.
Q4. What does the built-in id() function return?
- The object's type name as a string
- An integer that is guaranteed unique among currently alive objects during the program's run — in CPython, typically the object's memory address
- A cryptographic hash of the object's contents
- The line number where the object was created
Show Answer
Answer: B — An integer that is guaranteed unique among currently alive objects during the program's run — in CPython, typically the object's memory address
Explanation: id() returns a value guaranteed to be unique and constant for an object for its lifetime; CPython implements this as the object's memory address, but that detail is implementation-specific and should not be relied on beyond identity comparison. Option A describes type(obj).__name__. Option C describes hash(), which is a different, value-based concept and is not guaranteed unique. Option D describes nothing Python tracks by default.
Q5. Which of these is the correct classification of built-in types by mutability?
- Mutable:
int,str,tuple— Immutable:list,dict,set - Mutable:
list,dict,set,bytearray— Immutable:int,float,str,tuple,bool,frozenset - All built-in container types are mutable; only numbers are immutable
- Mutability is a runtime flag you must set explicitly with
mutable=True
Show Answer
Answer: B — Mutable: list, dict, set, bytearray — Immutable: int, float, str, tuple, bool, frozenset
Explanation: list, dict, set, and bytearray support in-place mutation (append, __setitem__, add, and friends), whereas int, float, str, tuple, bool, and frozenset cannot be changed after creation — any "modification" actually produces a new object. Option A has the classification exactly backwards. Option C is wrong because tuple and frozenset are containers that are nonetheless immutable. Option D describes a mechanism Python does not have — mutability is a property of the type, not a per-instance flag.
Q6. What is the difference between type(obj) == SomeClass and isinstance(obj, SomeClass) when obj might be an instance of a subclass?
class Animal: pass
class Dog(Animal): pass
d = Dog()
- They always behave identically for every object
-
type(d) == AnimalisFalsefor aDoginstance, whileisinstance(d, Animal)isTrue, becauseisinstanceaccounts for inheritance -
type(d) == AnimalisTruebecause Python treats subclasses as equal types -
isinstanceonly works with built-in types, not user-defined classes
Show Answer
Answer: B — type(d) == Animal is False for a Dog instance, while isinstance(d, Animal) is True, because isinstance accounts for inheritance
Explanation: type(d) returns the exact class Dog, which does not equal Animal, whereas isinstance walks the MRO and correctly reports that a Dog "is-an" Animal. This distinction matters constantly in real code that accepts subclasses polymorphically. Option A ignores exactly this divergence. Option C misunderstands type(), which never treats a subclass as equal to its parent. Option D is false — isinstance works uniformly across built-in and user-defined classes.
Q7. What happens when you execute b = a where a is a list?
a = [1, 2, 3]
b = a
- A new, independent copy of the list is created and bound to
b -
bbecomes a second name bound to the exact same list object asa; no copying occurs -
bis bound to a lazily-copied "view" that only copies on first mutation - This raises a
TypeErrorbecause lists cannot be reassigned
Show Answer
Answer: B — b becomes a second name bound to the exact same list object as a; no copying occurs
Explanation: Assignment in Python never copies an object; it binds a name to whatever object is on the right-hand side. So a and b here are two labels for one list, and a is b is True — mutating through either name affects what the other sees. Option A describes copy semantics found in some other languages, not Python's default =. Option C describes copy-on-write, a strategy CPython does not use for list assignment. Option D is false; reassignment is always legal.
Q8. Given the following, what is printed, and why?
a = 100
b = 100
print(a is b)
x = 1000
y = 1000
print(x is y)
-
TruethenTrue— CPython always caches and reuses every integer object -
TruethenFalse— CPython pre-caches and interns small integers in the range -5 to 256, but not arbitrary larger integers, which may or may not be separate objects depending on context -
FalsethenFalse— integers are never cached in CPython -
FalsethenTrue— only large integers are cached to save memory
Show Answer
Answer: B — True then False — CPython pre-caches and interns small integers in the range -5 to 256, but not arbitrary larger integers, which may or may not be separate objects depending on context
Explanation: Performance — as a memory optimization, CPython pre-allocates and reuses a singleton object for every integer from -5 to 256, so a is b is reliably True for 100. Numbers outside that range are ordinarily created fresh each time (x is y is typically False, though this is an implementation detail that can vary — e.g. constant-folding within the same compiled code unit can sometimes make it True too). The trap is treating is as safe for comparing arbitrary integers just because it "worked" during testing with small numbers; always use == for integer value comparison.
Q9. What is the most accurate description of CPython string interning?
s1 = "hello"
s2 = "hello"
print(s1 is s2)
s3 = "".join(["h", "e", "l", "l", "o"])
print(s1 is s3)
- All strings with equal content are always the same object, no matter how they are constructed
- Compile-time string literals that look like identifiers are commonly interned and may share identity, but strings built at runtime (e.g. via concatenation or
join) are typically distinct objects even with identical content - No strings are ever interned in CPython;
ison strings is alwaysFalse - Interning applies only to numeric strings like
"123"
Show Answer
Answer: B — Compile-time string literals that look like identifiers are commonly interned and may share identity, but strings built at runtime (e.g. via concatenation or join) are typically distinct objects even with identical content
Explanation: Debug — CPython, as an optimization, often interns short literals that resemble identifiers at compile time, so s1 is s2 frequently prints True. But s3, built at runtime via "".join(...), is a freshly allocated string object even though it's equal in content, so s1 is s3 is False. This is a classic footgun: code that appears to work using is for string comparison in a quick test can silently break once the string is produced dynamically (e.g., from user input, formatting, or + concatenation). Always compare string values with ==, never is.
Q10. What does the following print, and why is it surprising?
n = float("nan")
print(n == n)
print(n is n)
-
TruethenTrue— a value always equals itself -
FalsethenTrue— per IEEE 754, NaN never equals itself under==, butiscompares identity, and it's the same object bound tonboth times -
FalsethenFalse— NaN breaks both identity and equality checks -
TruethenFalse— NaN is equal to itself but Python creates a new object on each reference
Show Answer
Answer: B — False then True — per IEEE 754, NaN never equals itself under ==, but is compares identity, and it's the same object bound to n both times
Explanation: Debug — NaN != NaN is mandated by the IEEE 754 floating-point standard (any comparison involving NaN except != is False), so n == n is False even though n is literally the same object as itself. n is n is True because identity doesn't care about the value-equality rules at all — it's trivially the same object. This is a real production gotcha: x == x is not a safe way to detect NaN; use math.isnan(x) instead, and remember NaN in some_list can silently fail to find a NaN it should logically match.
Q11. Do two independently created empty tuples share identity?
t1 = ()
t2 = ()
print(t1 is t2)
- No — every tuple literal, even an empty one, allocates a new object
- Yes — CPython caches a singleton empty tuple, so both names typically refer to the same object
- Only inside function bodies, never at module level
- Only if both are explicitly declared with
tuple()instead of()
Show Answer
Answer: B — Yes — CPython caches a singleton empty tuple, so both names typically refer to the same object
Explanation: Performance — because the empty tuple is immutable and has no meaningful internal state to diverge, CPython optimizes by reusing one singleton empty-tuple object everywhere, so t1 is t2 is True. This is a CPython implementation detail (not a language guarantee), unlike, say, empty lists, where [] is [] is False because lists are mutable and must never be silently shared. The lesson generalizes: never write code whose correctness depends on such identity caching — use == for comparisons regardless.
Q12. A function receives a mutable list and appends to it, but also reassigns the parameter name inside the function. What does the caller observe?
def process(items):
items.append("processed")
items = ["replaced"]
data = ["order-1"]
process(data)
print(data)
-
["replaced"]— the reassignment inside the function propagates back to the caller -
["order-1", "processed"]— the in-placeappendmutates the shared object the caller sees, but rebinding the local nameitemsonly affects the local scope, not the caller'sdata -
["order-1"]— function calls never affect the caller's objects - A
TypeError, because you cannot both mutate and reassign a parameter in the same function
Show Answer
Answer: B — ["order-1", "processed"] — the in-place append mutates the shared object the caller sees, but rebinding the local name items only affects the local scope, not the caller's data
Explanation: Python passes arguments by binding the parameter name to the same object the caller passed ("pass by object reference"). items.append(...) mutates that shared list, so the caller sees it. But items = ["replaced"] merely rebinds the local name items to point at a brand-new list — it does not, and cannot, reach back and change what data points to. Beginners often expect either "everything propagates" (A) or "nothing propagates" (C); the real behavior is a mix, and it's the single most common source of confusion around Python's argument-passing model. Option D describes a restriction that doesn't exist.
Q13. What does the following print, and what does it reveal about bool?
print(True == 1)
print(isinstance(True, int))
print(True + True)
-
False,False,TypeError— booleans are unrelated to integers -
True,True,2—boolis a subclass ofintin Python, soTrue/Falsebehave as1/0in arithmetic and comparisons -
True,False,TypeError— booleans equal integers by value but are not related by type -
False,True,2—boolinherits fromintbut the values are never equal
Show Answer
Answer: B — True, True, 2 — bool is a subclass of int in Python, so True/False behave as 1/0 in arithmetic and comparisons
Explanation: Debug — bool is literally a subclass of int, with True and False behaving as 1 and 0 respectively; that's why True == 1 is True, isinstance(True, int) is True, and True + True evaluates to 2 without error. This surprises people who assume booleans are a wholly separate type. It has real consequences: sum([True, False, True]) yields 2, and {1: "a", True: "b"} collapses to a single key because 1 == True and hash(1) == hash(True).
Q14. Is it safe to compare id(obj_a) == id(obj_b) for two objects that existed at different, non-overlapping points in a long-running program to conclude they were "the same object"?
- Yes,
id()values are globally unique for all time, so a match always proves it was the same object - No — once an object is garbage-collected, CPython may reuse its freed memory address for a completely unrelated new object, so a matching
id()across non-overlapping lifetimes proves nothing - No, because
id()is randomized on every call and never repeats - Yes, but only for immutable types
Show Answer
Answer: B — No — once an object is garbage-collected, CPython may reuse its freed memory address for a completely unrelated new object, so a matching id() across non-overlapping lifetimes proves nothing
Explanation: Safety — id() uniqueness is only guaranteed among objects that are alive at the same time; CPython's allocator is free to hand a freshly freed address to a brand-new, unrelated object. A long-running service that logs id() values to "track" objects across time can be misled into thinking two clearly different objects are the same one. The safe pattern is to keep a live reference (preventing garbage collection) for as long as identity needs to be checked, rather than persisting bare id() integers.
Q15. What is the idiomatic way to check whether obj is an instance of MyClass or one of its subclasses?
if isinstance(obj, MyClass):
...
-
if type(obj) == MyClass: -
if isinstance(obj, MyClass): -
if obj.__class__.__name__ == "MyClass": -
if str(type(obj)) == "MyClass":
Show Answer
Answer: B — if isinstance(obj, MyClass):
Explanation: Idiom — isinstance is the idiomatic, subclass-aware check and also gracefully supports checking against a tuple of types (isinstance(obj, (int, float))). Option A silently excludes legitimate subclass instances, which routinely breaks polymorphic code paths (e.g., custom exceptions or ORM model subclasses). Options C and D are fragile string-matching hacks that break under refactors, module renames, or subclassing, and are never the recommended approach.
Q16. What is the idiomatic way to check whether a variable is None?
if value is None:
...
-
if value == None: -
if value is None: -
if not value: -
if value.__eq__(None):
Show Answer
Answer: B — if value is None:
Explanation: Idiom — None is a singleton, so identity comparison is both correct and faster than an equality check, and PEP 8 explicitly recommends is/is not for None comparisons. Option A technically often works too (since None.__eq__ falls back to identity-like behavior for the default case), but it's non-idiomatic and, critically, an object could override __eq__ to claim it equals None, silently breaking an == check in a way is never can. Option C is a different, broader check — it also matches falsy values like 0, "", and [], which is a common and dangerous conflation with "is None". Option D is needlessly indirect and bypasses Python's reflected-comparison protocol.
Q17. Should application logic ever rely on CPython's small-integer or string-interning caching (i.e., using is where == is meant)?
- Yes — it's a documented, guaranteed language feature safe to depend on
- No — it is a CPython implementation detail that can change between versions/implementations (e.g., PyPy) and even between contexts (interactive shell vs. module vs.
-Ooptimizations); use==for value comparisons - Yes, but only for integers, never for strings
- It doesn't matter, since
isand==always agree for built-in immutable types
Show Answer
Answer: B — No — it is a CPython implementation detail that can change between versions/implementations (e.g., PyPy) and even between contexts (interactive shell vs. module vs. -O optimizations); use == for value comparisons
Explanation: Portability — small-int caching and string interning are documented as CPython optimizations, not language guarantees; other implementations (PyPy, Jython) or even future CPython releases are free to cache differently, or not at all. Code that happens to pass tests because 100 is 100 was True can fail unpredictably in a different environment or with slightly larger numbers. Option D is directly contradicted by the x = 1000; y = 1000 example seen earlier in this quiz, where is and == diverge.
Q18. Which of the following can be used as a key in a dict?
config = {
("region", "us-east-1"): "primary",
}
- A
list, since dict keys can be any object - A
tupleof hashable elements, like("region", "us-east-1"), since it is immutable and hashable - A
dict, since nesting dictionaries is common - A
set, since sets are also collections
Show Answer
Answer: B — A tuple of hashable elements, like ("region", "us-east-1"), since it is immutable and hashable
Explanation: Dict keys must be hashable, which in practice means immutable (or at least implementing a stable __hash__); a tuple of hashable elements qualifies and is a common composite-key pattern. list, dict, and set are all mutable and unhashable by default, so using any of them as a key raises TypeError: unhashable type. This ties directly back to mutability: a hash must stay constant for an object's lifetime, and a mutable object's contents — and thus its "natural" hash — could change after insertion, which would corrupt the hash table.
Q19. What is the correct, defensive way to convert untrusted user input like "abc" to an int?
raw = input("Enter age: ")
try:
age = int(raw)
except ValueError:
age = None
- Call
int(raw)directly and trust it will always succeed - Wrap the conversion in
try/except ValueError(as shown), sinceint()raisesValueErroron non-numeric strings rather than returning a sentinel - Use
float(raw)instead, since it never raises exceptions - Check
raw.isdigit()only, since that alone guaranteesint()will succeed for any valid integer, including negatives
Show Answer
Answer: B — Wrap the conversion in try/except ValueError (as shown), since int() raises ValueError on non-numeric strings rather than returning a sentinel
Explanation: int() (and float()) raise ValueError for unparseable strings instead of returning None or 0, so unguarded conversion of external input is a production crash waiting to happen — catching ValueError is the correct handling. Option A ignores that risk entirely. Option C is wrong on its face — float() raises ValueError too (e.g., float("abc")). Option D is a subtle trap: str.isdigit() returns False for a leading - sign ("-5".isdigit() is False), so relying on it alone would reject legitimate negative integers even though int("-5") succeeds fine.
Q20. Two engineers are debugging why mutating settings_copy also changed settings elsewhere in a large codebase. What is the most likely root cause?
settings = {"debug": False}
settings_copy = settings
settings_copy["debug"] = True
print(settings["debug"])
-
settings_copy = settingssilently created a deep copy, and the bug lies elsewhere -
settings_copy = settingsonly creates a new alias to the same dict object — no copy was ever made, so mutating one mutates both; a real copy requiresdict(settings),settings.copy(), orcopy.deepcopy(settings)for nested structures -
dictobjects are immutable, so this code should have raised an exception - This is a garbage collector bug and cannot be fixed in user code
Show Answer
Answer: B — settings_copy = settings only creates a new alias to the same dict object — no copy was ever made, so mutating one mutates both; a real copy requires dict(settings), settings.copy(), or copy.deepcopy(settings) for nested structures
Explanation: Idiom — this is the single most common real-world "why did my data change on its own" bug: plain = never copies a mutable object, it only adds another name pointing at the same one. The fix is to explicitly request a copy — a shallow copy (dict(settings) / settings.copy()) is enough for a flat dict of immutable values, while nested mutable structures need copy.deepcopy. Option A describes behavior = simply does not have. Option C is false — dict is mutable by design. Option D misattributes an application-level aliasing bug to the garbage collector, which is unrelated.