Advanced Concepts β Python Questions with Answers
Start with the study notes on Advanced Concepts (definitions, examples and a quick revision), then work through 30 practice questions, each with the correct answer and a clear explanation of why it is right. When you are ready, take this topic as an interactive quiz to test yourself.
0 easy Β· 10 medium Β· 20 hard Β· Part of Python: Master Python from basics to advanced - 20 topics.
Topic overview
Advanced Concepts β the tools experienced Python developers use
1. The GIL and running things in parallel
The GIL (Global Interpreter Lock) in CPython lets only one thread run Python code at a time.
threading and asyncio: good for I/O-bound work (waiting on the network or disk).
multiprocessing: separate processes, each with its own interpreter. Use it for CPU-heavy work on several cores.
2. Shallow copy vs deep copy
A shallow copy (.copy(), [:]) copies the outer list, but shares the inner objects.
A deep copy (copy.deepcopy()) copies everything, all the way down.
import copy
a = [[1, 2], [3, 4]]
shallow = a.copy()
deep = copy.deepcopy(a)
shallow[0][0] = 99
print(a)
print(deep)
Output[[99, 2], [3, 4]]
[[1, 2], [3, 4]]
3. Unpacking with * and **
a, *rest = items collects the leftover items into a list.
f(*my_list) spreads a list into arguments; f(**my_dict) spreads a dict into keyword arguments.
{**d1, **d2} merges dicts; the later one wins.
first, *middle, last = [1, 2, 3, 4]
print(first, middle, last)
def add(a, b, c):
return a + b + c
print(add(*[1, 2, 3]))
print({**{"x": 1, "y": 2}, **{"y": 99}})
Output1 [2, 3] 4
6
{'x': 1, 'y': 99}
4. The walrus operator := (Python 3.8+)
:=assigns and uses a value in the same expression.
data = [5, 12, 8, 20]
if (n := len(data)) > 3:
print(f"{n} items")
print([y for x in data if (y := x * 2) > 15])
Output4 items
[24, 16, 40]
5. Type hints
Type hints document the expected types. Tools such as mypy and IDEs check them, but Python does not enforce them at runtime. Optional[str] means βa str or Noneβ; in 3.10+ you can write str | None.
A context manager runs set-up code on entering a with block and clean-up code on leaving it, even if an error happens. Build one with __enter__/__exit__, or with @contextmanager.
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield
print(f"</{name}>")
with tag("b"):
print("hi")
Output<b>
hi
</b>
7. Handy standard-library tools
from collections import defaultdict
from functools import partial
from itertools import zip_longest
groups = defaultdict(list) # missing keys start as []
for name in ["ana", "amy", "bob"]:
groups[name[0]].append(name)
print(dict(groups))
cube = partial(pow, exp=3) # pre-fill an argument
print(cube(2))
matrix = [[1, 2], [3, 4]]
print(list(zip(*matrix))) # transpose
print(list(zip_longest([1, 2, 3], ["a"], fillvalue="?")))
f"{x=}" prints the expression and its value; great for debugging.
__slots__ fixes an object's attributes. It saves memory and blocks new attributes.
Monkey patching replaces code at runtime, e.g. in tests. Use it sparingly.
A metaclass is βthe class of a classβ; type is the default. Rarely needed.
x = 5
print(f"{x=}", f"{x * 2=}")
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
try:
p.z = 3
except AttributeError:
print("no new attributes allowed")
Outputx=5 x * 2=10
no new attributes allowed
9. is vs == (one more time)
== compares values; is checks whether two names are the same object. CPython reuses the small integers from -5 to 256, so is can look like it works for them, but never use is to compare numbers or strings. Use is only for None.
a = [1, 2]
b = [1, 2]
print(a == b, a is b)
print(sorted([3, 1]) is sorted([3, 1]))
OutputTrue False
False
10. Common misconceptions
Misconception
Correct understanding
Threads make CPU-heavy Python code faster.
The GIL allows one thread at a time; use multiprocessing for CPU work.
list.copy() copies the nested lists too.
It is a shallow copy; use copy.deepcopy().
Type hints stop wrong types.
Python ignores them at runtime; tools like mypy check them.
is works for comparing numbers.
Use == for values; is only for None.
Metaclasses are needed in everyday code.
They are rarely needed; normal classes cover almost everything.
11. Quick revision
GIL
Lets one thread run Python code at a time.
Shallow copy
Copies the outer object; shares inner ones.
Deep copy
Copies everything (copy.deepcopy).
*rest
Collects leftover items when unpacking.
:=
Walrus operator: assign inside an expression.
Type hints
Documented types; not enforced.
Context manager
Set-up and clean-up around a with block.
defaultdict
Dict that creates missing keys automatically.
12. Test your understanding
For CPU-heavy work, should you use threading or multiprocessing?
Show answer
multiprocessing (it bypasses the GIL).
What is the difference between a shallow and a deep copy?
Show answer
A shallow copy shares nested objects; a deep copy copies everything.
What do a and rest get from a, *rest = [1, 2, 3]?
Show answer
a = 1, rest = [2, 3].
What does the walrus operator := do?
Show answer
Assigns a value and uses it in the same expression.
Are type hints enforced at runtime?
Show answer
No; tools such as mypy check them.
What does with run when the block ends?
Show answer
The context manager's clean-up code, even after an error.
What does zip(*matrix) do?
Show answer
Transposes the matrix (rows become columns).
What does f"{x=}" print when x = 5?
Show answer
x=5
Key takeaway: Know when the GIL matters, copy deeply when data is nested, unpack with * and **, and let context managers and the standard library do the heavy lifting.
Notes written by Velnov. Every code example with an output was run in Python 3 to check that the output shown is exactly what Python prints.
What is the walrus operator := used for (Python 3.8+)?
while (line := input()) != "quit":
print(line)
# instead of:
# line = input()
# while line != 'quit': ... line = input()
A Assign a value AND use it in the same expression
B Compare two values
C Multiply and assign
D Unpack a tuple
Answer: A β Assign a value AND use it in the same expression
The walrus operator assigns within an expression, avoiding a separate line. Common in while loops and comprehensions where you'd otherwise compute a value twice.
Assignment expressionsPython 3.8
Q6.medium
What does *rest capture here?
a, *rest = [1, 2, 3, 4]
A a = 1 and rest = [2, 3, 4] - starred unpacking grabs the remaining items as a list
B a = [1, 2, 3, 4], rest = []
C a = 1, rest = 2
D Error
Answer: A β a = 1 and rest = [2, 3, 4] - starred unpacking grabs the remaining items as a list
Extended unpacking: the starred name collects all leftover elements into a list. It can appear anywhere: first, *middle, last = [1,2,3,4] gives 1, [2,3], 4.
Extended unpacking*args
Q8.medium
What are type hints, and are they enforced at runtime?
def f(x: int) -> str:
def greet(name: str) -> str:
return "Hi " + name
greet(123) # runs (then crashes on +), hints not enforced
A Annotations documenting expected types; NOT enforced - tools like mypy check them, Python ignores them
B Strict runtime type checks
C Comments only
D Required in Python 3
Answer: A β Annotations documenting expected types; NOT enforced - tools like mypy check them, Python ignores them
Hints improve readability and let static checkers (mypy, IDEs) catch bugs before running. But Python itself doesn't enforce them: f("hello") still runs. They're purely advisory at runtime.
Type hintsmypytyping module
Q13.medium
What is an f-string's = debug feature (Python 3.8+)?
print(f"{x=}")
x = 5
print(f"{x=}") # x=5
print(f"{x * 2=}") # x * 2=10
A Prints both the expression and its value: x=5
B Compares x to 5
C Assigns 5 to x
D Prints just the value
Answer: A β Prints both the expression and its value: x=5
f"{x=}" expands to 'x=5' - it shows the variable name AND value. A quick debugging shortcut that saves typing the name twice.
f-stringsDebugging
Q16.medium
What does zip(*matrix) do to matrix = [[1, 2], [3, 4]]?
A Transposes it: [(1, 3), (2, 4)] - rows become columns
B Flattens it
C Reverses it
D Sums the rows
Answer: A β Transposes it: [(1, 3), (2, 4)] - rows become columns
zip(*matrix) unpacks the rows as arguments to zip, pairing up matching positions - effectively transposing the matrix. A neat one-liner.
zip()UnpackingTranspose
Q19.medium
What does enumerate(items, start=1) change?
for i, name in enumerate(["a", "b"], start=1):
print(i, name) # 1 a / 2 b
A Numbering begins at 1 instead of 0
B It starts from the second item
C It reverses the items
D Nothing
Answer: A β Numbering begins at 1 instead of 0
The start argument sets the first index. enumerate(items, 1) is handy for user-facing numbered lists that should begin at 1.
enumerate()Indexing
Q22.medium
What does the ternary/conditional expression evaluate to?
x = a if condition else b
status = "adult" if age >= 18 else "minor"
A a when condition is True, otherwise b
B Always a
C A tuple (a, b)
D b when condition is True
Answer: A β a when condition is True, otherwise b
This is Python's inline if-else expression. It returns one of two values based on the condition, all in one line - great for concise assignments.
Conditional expressionTernary
Q25.medium
What does the else clause of a for loop do?
for x in items:
if x == target: break
else:
print("not found")
A Runs only if the loop finished WITHOUT hitting break
B Runs after every iteration
C Runs only when break happens
D Is a syntax error
Answer: A β Runs only if the loop finished WITHOUT hitting break
for-else: the else block runs when the loop completes normally (no break). Ideal for search loops - if break never fired, the item wasn't found.
for-elseSearch pattern
Q27.medium
What does any() with a generator do?
any(x > 100 for x in nums)
has_big = any(x > 100 for x in nums)
all_positive = all(x > 0 for x in nums)
A Returns True if at least one item satisfies the condition, stopping early on the first True
B Returns all matching items
C Counts the matches
D Returns the largest item
Answer: A β Returns True if at least one item satisfies the condition, stopping early on the first True
any() short-circuits: it stops as soon as one truthy value appears - efficient even on huge/infinite generators. all() is the opposite: True only if every item passes.
any()all()Short-circuit
Q29.medium
What does sorted(data, key=lambda x: x[1], reverse=True) do?
A Sorts data by each item's second element, in descending order
B Sorts alphabetically
C Reverses without sorting
D Sorts by the first element ascending
Answer: A β Sorts data by each item's second element, in descending order
key picks what to compare (element [1] of each item); reverse=True flips to descending. This is the standard way to sort tuples/dicts by a chosen field.
sorted()key functionslambda
Hard questions (20)
Q1.hard
What is the GIL (Global Interpreter Lock)?
A A lock that lets only ONE thread run Python bytecode at a time in CPython
B A security feature for imports
C A garbage collector
D A way to lock files
Answer: A β A lock that lets only ONE thread run Python bytecode at a time in CPython
In CPython, the GIL ensures only one thread executes Python bytecode at once. This means threads don't give true parallelism for CPU-bound work - but they still help I/O-bound tasks (waiting on network/disk).
For CPU-bound parallelism, use multiprocessing instead of threading.
ThreadingmultiprocessingCPython
Q2.hard
For CPU-heavy work needing real parallelism, which should you use?
A multiprocessing - separate processes each with their own interpreter, bypassing the GIL
B threading - always faster
C A bigger for loop
D asyncio
Answer: A β multiprocessing - separate processes each with their own interpreter, bypassing the GIL
Because the GIL blocks parallel bytecode execution, threads can't speed up CPU-bound work. multiprocessing spawns separate processes (each with its own GIL) that run on multiple cores. asyncio and threading suit I/O-bound work.
multiprocessingGILParallelism
Q3.hard
What is the difference between a shallow copy and a deep copy?
A Shallow copies the outer object but shares nested objects; deep copies everything recursively
B They are identical
C Deep copy is faster
D Shallow copy only works on strings
Answer: A β Shallow copies the outer object but shares nested objects; deep copies everything recursively
A shallow copy (list[:], .copy(), copy.copy) duplicates the top level but the inner objects are still shared. copy.deepcopy() recursively duplicates everything, so nested objects are independent too.
copy moduleReferences
Q4.hard
What is the output?
a = [[0, 0], [0, 0]]
b = a[:]
b[0][1] = 9
print(a[0][1])
A 9 - the slice is a shallow copy, so the inner lists are shared
B 0 - b is fully independent
C None
D Error
Answer: A β 9 - the slice is a shallow copy, so the inner lists are shared
a[:] copies only the outer list; both a and b point to the SAME inner lists. Modifying b[0][1] shows up in a. Use copy.deepcopy() for true independence of nested structures.
Shallow copyNested lists
Q7.hard
What does the ** operator do here?
combined = {**dict1, **dict2}
A Merges the two dicts into a new one; dict2's values win on key clashes
B Multiplies the dicts
C Raises dict1 to the power dict2
D Compares the dicts
Answer: A β Merges the two dicts into a new one; dict2's values win on key clashes
** unpacks a dict's key-value pairs. Inside a new dict literal it merges them; later keys override earlier ones. Also used to pass a dict as keyword arguments: func(**kwargs).
Dict unpacking**kwargs
Q9.hard
What does this type hint mean?
from typing import Optional
def find(id: int) -> Optional[str]:
Optional[str] means 'str or None' (shorthand for Union[str, None]). It signals a function that may return a value or nothing. In Python 3.10+ you can write str | None instead.
OptionalUniontyping
Q10.hard
What is a context manager?
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield
print(f"</{name}>")
with tag("b"):
print("hi")
A An object usable with 'with' that runs setup on entry and cleanup on exit (via __enter__/__exit__)
B A manager class for variables
C A type of decorator
D A threading tool
Answer: A β An object usable with 'with' that runs setup on entry and cleanup on exit (via __enter__/__exit__)
Context managers guarantee cleanup. 'with open(...) as f:' closes the file automatically, even on exceptions. You can build your own with __enter__/__exit__ or the @contextmanager decorator.
with statementcontextlib
Q11.hard
What is monkey patching?
import time
time.sleep = lambda s: None # patch away real sleeping in tests
A Modifying or replacing a class/module's attributes at RUNTIME
B Fixing bugs with a debugger
C A testing framework
D Writing messy code
Answer: A β Modifying or replacing a class/module's attributes at RUNTIME
Monkey patching changes code dynamically at runtime - e.g. replacing a method on an existing class, often in tests to swap out slow/external calls. Powerful but risky; use sparingly.
Runtime modificationTestingMocking
Q12.hard
What does __slots__ do in a class?
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
# p.z = 3 # AttributeError - not in __slots__
A Restricts instances to a fixed set of attributes, saving memory by removing the per-instance __dict__
B Creates database slots
C Adds time slots
D Makes the class abstract
Answer: A β Restricts instances to a fixed set of attributes, saving memory by removing the per-instance __dict__
Normally each instance stores attributes in a __dict__. __slots__ replaces it with a fixed layout: less memory per object (big when you have millions) and no new attributes allowed.
Memory optimization__dict__
Q14.hard
What does @dataclass reduce boilerplate for?
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print(Point(1, 2)) # Point(x=1, y=2)
A Auto-generating __init__, __repr__, and __eq__ from declared fields
B Database connections
C File handling
D Threading
Answer: A β Auto-generating __init__, __repr__, and __eq__ from declared fields
For classes that mainly hold data, @dataclass writes the constructor, a readable repr, and equality-by-fields for you. Declare the fields with type hints and you're done.
dataclassesBoilerplate
Q15.hard
What is the output?
from collections import defaultdict
d = defaultdict(list)
d["a"].append(1)
print(d["a"])
from collections import defaultdict
groups = defaultdict(list)
for name in ["ana", "amy", "bob"]:
groups[name[0]].append(name) # group by first letter
A [1] - defaultdict auto-creates an empty list for a missing key
B KeyError
C None
D []
Answer: A β [1] - defaultdict auto-creates an empty list for a missing key
defaultdict(list) makes missing keys spring into existence with a default (here, an empty list) instead of raising KeyError. Perfect for grouping items without checking 'if key in d' first.
defaultdictcollections
Q17.hard
What is the difference between is and == (recap for advanced code)?
a = [1, 2]; b = [1, 2]
print(a == b) # True - equal values
print(a is b) # False - different objects
A == compares values; is checks whether two names point to the exact same object
B They are the same
C is compares values
D == only works on numbers
Answer: A β == compares values; is checks whether two names point to the exact same object
is tests identity (same memory object). == tests equality of value. Use is ONLY for None, True, False and sentinel objects. Comparing values with is can give surprising results due to caching.
IdentityEquality
Q18.hard
Why can small ints behave oddly with is?
a = 256; b = 256; print(a is b) # True
a = 257; b = 257; print(a is b) # often False
A CPython caches small integers (-5 to 256), so identical small ints share one object; larger ones may not
B It's a bug
C 256 is special in binary
D is never works on numbers
Answer: A β CPython caches small integers (-5 to 256), so identical small ints share one object; larger ones may not
CPython pre-creates and reuses int objects from -5 to 256, so 'a is b' is True for those. Beyond that range, separate objects may be created. This is exactly why you compare numbers with ==, never is.
An implementation detail - never rely on it in real code.
Integer cachingis vs ==
Q20.hard
What is a generator's role in the memory-efficient processing of big data?
lines = (l.strip() for l in open("huge.log"))
errors = (l for l in lines if "ERROR" in l)
for e in errors: ... # constant memory
A It yields items lazily, so pipelines process huge datasets without loading everything into memory
B It compresses the data
C It stores data on disk
D It runs on the GPU
Answer: A β It yields items lazily, so pipelines process huge datasets without loading everything into memory
Chaining generator expressions builds a lazy pipeline: each item flows through filters/transforms one at a time. You can process files far larger than RAM. (Full topic 17.)
GeneratorsStreaming
Q21.hard
What does functools.partial do?
from functools import partial
square = partial(pow, exp=2)
A Creates a new function with some arguments pre-filled
B Splits a function in half
C Runs a function partially then pauses
D Caches results
Answer: A β Creates a new function with some arguments pre-filled
partial(func, *fixed_args) 'freezes' some arguments, returning a simpler function. Useful for adapting a general function to a specific use, e.g. as a callback.
functoolsCurrying
Q23.hard
What is the output?
print(list(zip([1, 2, 3], ["a", "b"])))
A [(1, 'a'), (2, 'b')] - zip stops at the SHORTER iterable
B [(1, 'a'), (2, 'b'), (3, None)]
C Error - lengths differ
D [(1, 2, 3), ('a', 'b')]
Answer: A β [(1, 'a'), (2, 'b')] - zip stops at the SHORTER iterable
zip pairs items until the shortest input runs out - the leftover 3 is dropped. Use itertools.zip_longest if you want to pad with a fill value instead.
zip()zip_longest
Q24.hard
What are *args and **kwargs at a CALL site (unpacking)?
f(*my_list, **my_dict)
def add(a, b, c): return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 6
A * spreads a list into positional arguments; ** spreads a dict into keyword arguments
B They multiply the arguments
C They create new lists
D They are only valid in definitions
Answer: A β * spreads a list into positional arguments; ** spreads a dict into keyword arguments
At a call, * and ** unpack: f(*[1,2,3]) is f(1,2,3), and f(**{'a':1}) is f(a=1). The mirror image of collecting them with *args/**kwargs in a definition.
Unpacking*args**kwargs
Q26.hard
What is the output?
x = 10
def f():
print(x)
x = 20
f()
A UnboundLocalError - assigning x anywhere makes it local for the whole function
B 10
C 20
D None
Answer: A β UnboundLocalError - assigning x anywhere makes it local for the whole function
Because x is assigned inside f (x = 20), Python treats x as local throughout f - so print(x) references a local that hasn't been assigned yet. Fix with 'global x' or rename.
UnboundLocalErrorScopeLEGB
Q28.hard
What is a metaclass, in one line?
class Meta(type): ...
class MyClass(metaclass=Meta): ...
A The 'class of a class' - it controls how classes themselves are created (type is the default metaclass)
B A class with metadata
C A very large class
D An abstract base class
Answer: A β The 'class of a class' - it controls how classes themselves are created (type is the default metaclass)
Just as objects are instances of classes, classes are instances of metaclasses. type is the default. Metaclasses customize class creation - a deep, rarely-needed feature (frameworks use them). 'If you wonder whether you need them, you don't.'
Metaclassestype
Q30.hard
What is the output?
print([1, 2, 3] * 2)
print((1, 2) + (3,))
A [1, 2, 3, 1, 2, 3] and (1, 2, 3)
B [2, 4, 6] and (4, 5)
C Error
D [1, 2, 3, 2] and (1, 5)
Answer: A β [1, 2, 3, 1, 2, 3] and (1, 2, 3)
* repeats a sequence; + concatenates. Works on both lists and tuples: [1,2,3]*2 duplicates the list, and (1,2)+(3,) joins tuples (note the trailing comma makes (3,) a one-tuple).