πŸŽ“ Velnov

🐍 Python · Topic 19 of 20

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.

2. Shallow copy vs deep copy

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 **

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.

from typing import Optional

def find(names: dict[int, str], id: int) -> Optional[str]:
    return names.get(id)

print(find({1: "Ana"}, 1), find({1: "Ana"}, 2))
OutputAna None

6. Context managers

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="?")))
Output{'a': ['ana', 'amy'], 'b': ['bob']}
8
[(1, 3), (2, 4)]
[(1, 'a'), (2, '?'), (3, '?')]

8. Small features worth knowing

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

MisconceptionCorrect 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

  1. For CPU-heavy work, should you use threading or multiprocessing?

    Show answer

    multiprocessing (it bypasses the GIL).

  2. What is the difference between a shallow and a deep copy?

    Show answer

    A shallow copy shares nested objects; a deep copy copies everything.

  3. What do a and rest get from a, *rest = [1, 2, 3]?

    Show answer

    a = 1, rest = [2, 3].

  4. What does the walrus operator := do?

    Show answer

    Assigns a value and uses it in the same expression.

  5. Are type hints enforced at runtime?

    Show answer

    No; tools such as mypy check them.

  6. What does with run when the block ends?

    Show answer

    The context manager's clean-up code, even after an error.

  7. What does zip(*matrix) do?

    Show answer

    Transposes the matrix (rows become columns).

  8. 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.

Key concepts in this topic

ThreadingmultiprocessingCPythonGILParallelismcopy moduleReferencesShallow copyNested listsAssignment expressionsPython 3.8Extended unpacking*argsDict unpacking**kwargsType hintsmypytyping moduleOptionalUniontypingwith statementcontextlibRuntime modification

Jump to: Medium Β· Hard

Medium questions (10)

Q5. medium

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()
  1. A Assign a value AND use it in the same expression
  2. B Compare two values
  3. C Multiply and assign
  4. 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, *rest = [1, 2, 3, 4]
print(a, rest)          # 1 [2, 3, 4]
first, *mid, last = [1, 2, 3, 4]
print(mid)               # [2, 3]
  1. A a = 1 and rest = [2, 3, 4] - starred unpacking grabs the remaining items as a list
  2. B a = [1, 2, 3, 4], rest = []
  3. C a = 1, rest = 2
  4. 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
  1. A Annotations documenting expected types; NOT enforced - tools like mypy check them, Python ignores them
  2. B Strict runtime type checks
  3. C Comments only
  4. 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
  1. A Prints both the expression and its value: x=5
  2. B Compares x to 5
  3. C Assigns 5 to x
  4. 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]]?

matrix = [[1, 2], [3, 4]]
print(list(zip(*matrix)))   # [(1, 3), (2, 4)]
  1. A Transposes it: [(1, 3), (2, 4)] - rows become columns
  2. B Flattens it
  3. C Reverses it
  4. 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
  1. A Numbering begins at 1 instead of 0
  2. B It starts from the second item
  3. C It reverses the items
  4. 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"
  1. A a when condition is True, otherwise b
  2. B Always a
  3. C A tuple (a, b)
  4. 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")

  1. A Runs only if the loop finished WITHOUT hitting break
  2. B Runs after every iteration
  3. C Runs only when break happens
  4. 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)
  1. A Returns True if at least one item satisfies the condition, stopping early on the first True
  2. B Returns all matching items
  3. C Counts the matches
  4. 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?

pairs = [("a", 3), ("b", 1)]
print(sorted(pairs, key=lambda x: x[1]))   # [('b', 1), ('a', 3)]
  1. A Sorts data by each item's second element, in descending order
  2. B Sorts alphabetically
  3. C Reverses without sorting
  4. 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)?

  1. A A lock that lets only ONE thread run Python bytecode at a time in CPython
  2. B A security feature for imports
  3. C A garbage collector
  4. 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?

  1. A multiprocessing - separate processes each with their own interpreter, bypassing the GIL
  2. B threading - always faster
  3. C A bigger for loop
  4. 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?

import copy
a = [[1, 2], [3, 4]]
b = a.copy()          # shallow
b[0][0] = 99
print(a)               # [[99, 2], [3, 4]] - inner shared!
c = copy.deepcopy(a)   # fully independent
  1. A Shallow copies the outer object but shares nested objects; deep copies everything recursively
  2. B They are identical
  3. C Deep copy is faster
  4. 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])

  1. A 9 - the slice is a shallow copy, so the inner lists are shared
  2. B 0 - b is fully independent
  3. C None
  4. 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 = {"x": 1, "y": 2}
b = {"y": 99, "z": 3}
print({**a, **b})   # {'x': 1, 'y': 99, 'z': 3}
  1. A Merges the two dicts into a new one; dict2's values win on key clashes
  2. B Multiplies the dicts
  3. C Raises dict1 to the power dict2
  4. 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]:

def find_user(id: int) -> Optional[str]:
    return names.get(id)   # str or None
  1. A The function returns a str OR None
  2. B The argument is optional
  3. C The function might not exist
  4. D It returns an optional number

Answer: A β€” The function returns a str OR None

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")
  1. A An object usable with 'with' that runs setup on entry and cleanup on exit (via __enter__/__exit__)
  2. B A manager class for variables
  3. C A type of decorator
  4. 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
  1. A Modifying or replacing a class/module's attributes at RUNTIME
  2. B Fixing bugs with a debugger
  3. C A testing framework
  4. 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__
  1. A Restricts instances to a fixed set of attributes, saving memory by removing the per-instance __dict__
  2. B Creates database slots
  3. C Adds time slots
  4. 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)
  1. A Auto-generating __init__, __repr__, and __eq__ from declared fields
  2. B Database connections
  3. C File handling
  4. 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
  1. A [1] - defaultdict auto-creates an empty list for a missing key
  2. B KeyError
  3. C None
  4. 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
  1. A == compares values; is checks whether two names point to the exact same object
  2. B They are the same
  3. C is compares values
  4. 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

  1. A CPython caches small integers (-5 to 256), so identical small ints share one object; larger ones may not
  2. B It's a bug
  3. C 256 is special in binary
  4. 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
  1. A It yields items lazily, so pipelines process huge datasets without loading everything into memory
  2. B It compresses the data
  3. C It stores data on disk
  4. 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)

from functools import partial
cube = partial(pow, exp=3)
print(cube(2))   # 8
  1. A Creates a new function with some arguments pre-filled
  2. B Splits a function in half
  3. C Runs a function partially then pauses
  4. 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"])))

from itertools import zip_longest
print(list(zip_longest([1,2,3], ["a","b"], fillvalue="?")))
# [(1,'a'), (2,'b'), (3,'?')]
  1. A [(1, 'a'), (2, 'b')] - zip stops at the SHORTER iterable
  2. B [(1, 'a'), (2, 'b'), (3, None)]
  3. C Error - lengths differ
  4. 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
  1. A * spreads a list into positional arguments; ** spreads a dict into keyword arguments
  2. B They multiply the arguments
  3. C They create new lists
  4. 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()

  1. A UnboundLocalError - assigning x anywhere makes it local for the whole function
  2. B 10
  3. C 20
  4. 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): ...
  1. A The 'class of a class' - it controls how classes themselves are created (type is the default metaclass)
  2. B A class with metadata
  3. C A very large class
  4. 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,))

  1. A [1, 2, 3, 1, 2, 3] and (1, 2, 3)
  2. B [2, 4, 6] and (4, 5)
  3. C Error
  4. 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).

Sequence operationsTuples

Practice β€œAdvanced Concepts” as a quiz