Skip to content

Python Programming Practice Test — 30 Problems

Python Programming Practice Test — 30 Problems

Section titled “Python Programming Practice Test — 30 Problems”

This practice test covers 30 problems across four major domains of Python programming: Syntax and Fundamentals, Data Structures, Object-Oriented Programming, and Algorithms and Standard Library. Each problem tests code analysis, debugging, and understanding of Python semantics. Work through all problems before checking the answer key.

  • Time limit: 90 minutes (3 minutes per problem)
  • Format: Code analysis and debugging — trace the output, identify errors, or select the correct implementation
  • Marking: 1 mark per problem, 30 marks total
  • Conditions: Attempt without notes. Trace code by hand.
  • After the test: Check the answer key at the bottom. Study the explanations for any problems you got wrong.
DomainProblemsMarks
Syntax and FundamentalsP1–P88
Data StructuresP9–P157
Object-Oriented ProgrammingP16–P227
Algorithms and Standard LibraryP23–P308
Total3030

What is the output?

def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c())
#Option
A1 1 1
B0 1 2
C1 2 3
DUnboundLocalError
E0 0 0

Correct: C (index 2)

The closure captures count from make_counter. The nonlocal keyword allows counter to modify the enclosing scope’s count. Each call increments count by 1. The first call returns 1, the second 2, the third 3.

easy — 1 mark


What is the output?

def append_to(element, target=[]):
target.append(element)
return target
print(append_to(1))
print(append_to(2))
#Option
A[1] then [2]
B[1] then [1, 2]
C[1, 2] then [1, 2]
DIndexError
E[2] then [1, 2]

Correct: B (index 1)

The default argument target=[] is evaluated once at function definition, not at each call. Both calls share the same list object. First call appends 1, returning [1]. Second call appends 2 to the same list, returning [1, 2]. This is a classic Python gotcha — use None as default and create the list inside the function.

medium — 1 mark


What is the output?

nums = (x * x for x in range(5))
total = sum(nums)
print(total)
print(sum(nums))
#Option
A30 then 30
B30 then 0
C0 then 0
D14 then 0
ETypeError

Correct: B (index 1)

Generator expressions are consumed once and exhausted. sum(nums) iterates through all values (0+1+4+9+16 = 30). After the first sum, the generator is exhausted. The second sum sees an empty generator and returns 0.

medium — 1 mark


What is the output?

a = [1, 2, 3, 4, 5]
b = a[1:4]
b[0] = 99
print(a)
print(b)
#Option
A[1, 99, 3, 4, 5] and [99, 3, 4]
B[1, 2, 3, 4, 5] and [99, 3, 4]
C[1, 2, 3, 4, 5] and [2, 3, 4]
D[1, 99, 99, 99, 5] and [99, 99, 99]
EIndexError

Correct: B (index 1)

Slicing creates a shallow copy of the selected portion. b = a[1:4] creates a new list [2, 3, 4]. Modifying b[0] does not affect a. a remains [1, 2, 3, 4, 5]; b becomes [99, 3, 4].

easy — 1 mark


What is the output?

import re
text = "Contact us at support@example.com"
if (m := re.search(r'[\w.]+@[\w.]+', text)):
print(m.group())
#Option
Asupport@example.com
BNone
CTypeError
DTrue
ENothing is printed

Correct: A (index 0)

The walrus operator := assigns the result of re.search() to m and returns the value. If the match is truthy (not None), the if-block executes and prints the matched email address. This avoids calling re.search() twice (once in the condition, once in the body).

easy — 1 mark


What is the output?

def bold(func):
def wrapper():
return "<b>" + func() + "</b>"
return wrapper
def italic(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper
@bold
@italic
def greet():
return "hi"
print(greet())
#Option
A<i><b>hi</b></i>
B<b><i>hi</i></b>
C<b>hi</b>
D<i>hi</i>
Ehi

Correct: B (index 1)

Decorators apply bottom-up: @italic wraps greet first, then @bold wraps the result. When called, bold’s wrapper executes first (adds <b>), then calls italic’s wrapper (adds <i>), which calls the original greet. Result: <b><i>hi</i></b>.

medium — 1 mark


P7 — Dictionary Comprehension with Condition

Section titled “P7 — Dictionary Comprehension with Condition”

What is the output?

data = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
result = {k: v for k, v in data.items() if v % 2 == 0}
print(result)
#Option
A{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
B{'b': 2, 'd': 4}
C{'a': 1, 'c': 3, 'e': 5}
D['b', 'd']
E2

Correct: B (index 1)

The dictionary comprehension filters items where the value is even (v % 2 == 0). Only 'b': 2 and 'd': 4 satisfy this condition. The result is a new dictionary containing only the even-valued entries.

easy — 1 mark


What is the output?

a = "hello"
b = "hello"
c = "".join(["h", "e", "l", "l", "o"])
print(a is b)
print(a is c)
#Option
ATrue then True
BFalse then False
CTrue then False
DFalse then True
ETrue then True (always)

Correct: C (index 2)

Python interns small strings and string literals — a and b reference the same interned object, so a is b is True. c is constructed at runtime via join, creating a new string object. Even though c has the same content, it is a different object, so a is c is False.

medium — 1 mark


What is the time complexity of x in my_list for a Python list?

#Option
AO(1)O(1)
BO(logn)O(\log n)
CO(n)O(n)
DO(nlogn)O(n \log n)
EDepends on element type

Correct: C (index 2)

Python lists are arrays. Checking membership (in) requires a linear scan of all elements, making it O(n). For frequent membership testing, convert to a set for O(1) average-case lookup.

easy — 1 mark


Since which Python version do dictionaries preserve insertion order?

#Option
APython 3.0
BPython 3.4
CPython 3.6 (implementation detail)
DPython 3.7 (guaranteed by language spec)
EPython 2.7

Correct: D (index 3)

Python 3.6 made insertion order preservation an implementation detail of CPython. Python 3.7 made it a language guarantee — all conforming implementations must preserve insertion order. Before 3.7, dictionary order was arbitrary.

medium — 1 mark


What is the output?

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a - b)
print(a | b)
print(a & b)
#Option
A{1, 2} {1, 2, 3, 4, 5, 6} {3, 4}
B{1, 2} {3, 4} {1, 2, 5, 6}
C{1, 2, 5, 6} {3, 4} {1, 2}
D{1, 2} {1, 2, 3, 4, 5, 6} {}
E{5, 6} {1, 2, 3, 4, 5, 6} {1, 2, 3, 4}

Correct: A (index 0)

a - b is the difference: elements in a but not in b = {1, 2}. a | b is the union: all elements from both = {1, 2, 3, 4, 5, 6}. a & b is the intersection: elements in both = {3, 4}.

easy — 1 mark


Which operation is O(1) for collections.deque but O(n) for list?

#Option
AAppend to end
BIndex access
CInsert at beginning
DLength check
EIteration

Correct: C (index 2)

deque is a doubly-linked list optimised for operations at both ends. Inserting at the beginning is O(1) for deque but O(n) for list (requires shifting all elements). Both have O(1) append to end. deque has O(n) index access; list has O(1).

medium — 1 mark


What is the output?

from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
c = Counter(words)
print(c.most_common(2))
#Option
A[('apple', 3), ('banana', 2)]
B[('banana', 2), ('cherry', 1)]
C{'apple': 3, 'banana': 2, 'cherry': 1}
D3
Eapple

Correct: A (index 0)

Counter counts hashable objects. most_common(2) returns the 2 most common elements as a list of (element, count) tuples. “apple” appears 3 times, “banana” 2 times, so the result is [('apple', 3), ('banana', 2)].

easy — 1 mark


What is the output?

data = {
"users": {
"alice": {"age": 30, "active": True},
"bob": {"age": 25, "active": False}
}
}
active_users = [name for name, info in data["users"].items() if info["active"]]
print(active_users)
#Option
A[{'age': 30, 'active': True}]
B['alice']
C['alice', 'bob']
D[30]
ETrue

Correct: B (index 1)

The list comprehension iterates over data["users"].items(). For each user, it checks if info["active"] is True. Only “alice” has active: True. The comprehension collects the keys (names), producing ['alice'].

medium — 1 mark


What is the output?

pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
result = []
for num, letter in pairs:
result.append(f"{letter}{num}")
print(result)
#Option
A[1, 2, 3]
B['a', 'b', 'c']
C['a1', 'b2', 'c3']
D[(1, 'a'), (2, 'b'), (3, 'c')]
E['1a', '2b', '3c']

Correct: C (index 2)

Tuple unpacking in the for loop assigns num and letter from each pair. The f-string f"{letter}{num}" places the letter first, then the number: “a1”, “b2”, “c3”.

easy — 1 mark


What is the output?

class A:
def greet(self): print("A", end=" ")
class B(A):
def greet(self): print("B", end=" ")
class C(A):
def greet(self): print("C", end=" ")
class D(B, C):
pass
D().greet()
#Option
AA
BB
CC
DD
EA B C

Correct: B (index 1)

Python uses C3 linearization (Method Resolution Order). For D(B, C), the MRO is D -> B -> C -> A. D().greet() looks up greet in MRO order. B has greet, so it prints “B”. This is Python’s solution to the diamond problem.

medium — 1 mark


What is the output?

class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
t = Temperature(100)
print(t.fahrenheit)
t.fahrenheit = 212
#Option
A212.0
B100.0 then 212.0
C212.0 then error
D100.0
EAttributeError

Correct: E (index 4)

t.fahrenheit calls the property getter, returning 100 * 9/5 + 32 = 212.0. The property has no setter defined, so t.fahrenheit = 212 raises AttributeError: can't set attribute. Properties are read-only by default unless a setter is defined with @fahrenheit.setter.

medium — 1 mark


What does __slots__ do in a Python class?

#Option
APrevents instantiation of the class
BRestricts instance attributes to a fixed set, saving memory
CMakes all attributes private
DEnables multiple inheritance
EPrevents subclassing

Correct: B (index 1)

__slots__ replaces the instance __dict__ with a fixed set of attribute descriptors. This prevents dynamic attribute creation, saves memory (no per-instance dict), and provides a small speed improvement. Subclasses without __slots__ regain __dict__ unless they also define __slots__.

medium — 1 mark


What is the output?

from dataclasses import dataclass, field
@dataclass
class Config:
name: str
options: list = field(default_factory=list)
c1 = Config("app")
c2 = Config("app")
c1.options.append("debug")
print(c1.options)
print(c2.options)
#Option
A['debug'] then ['debug']
B['debug'] then []
C[] then []
DTypeError
E['debug'] then None

Correct: B (index 1)

field(default_factory=list) creates a new list for each instance. c1 and c2 each get their own independent list. Appending to c1.options does not affect c2.options. Without default_factory, all instances would share the same mutable default — a common bug.

medium — 1 mark


What is the output?

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): pass
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
# s = Shape() # uncommented
c = Circle(5)
print(c.area())
#Option
A78.5
BTypeError on line marked # s = Shape()
C0
DShape
ECompiler error

Correct: A (index 0)

Shape is abstract because it has an @abstractmethod. Uncommenting s = Shape() would raise TypeError because you cannot instantiate abstract classes. Circle implements area(), so Circle(5).area() returns 78.5.

easy — 1 mark


What is the output?

class ManagedFile:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
print(f"opening {self.filename}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"closing {self.filename}")
with ManagedFile("data.txt") as f:
print("processing")
#Option
Aopening data.txt processing closing data.txt
Bprocessing opening data.txt closing data.txt
Copening data.txt closing data.txt
Dprocessing
ETypeError

Correct: A (index 0)

The with statement calls __enter__ before the block (prints “opening”), executes the block (prints “processing”), then calls __exit__ after the block (prints “closing”). This is Python’s equivalent of try-with-resources. __exit__ is called even if an exception occurs.

easy — 1 mark


What does a metaclass control?

#Option
AInstance method resolution
BHow classes are created and configured
CObject destruction order
DModule import mechanism
EGarbage collection

Correct: B (index 1)

A metaclass is the “class of a class” — it controls how classes are created. When Python encounters class Foo(metaclass=MyMeta), it calls MyMeta('Foo', bases, namespace). Metaclasses can modify the class namespace, enforce invariants, register classes, or inject methods. They are used by frameworks like Django ORM, SQLAlchemy, and Pydantic.

medium — 1 mark


Algorithms and Standard Library (P23–P30)

Section titled “Algorithms and Standard Library (P23–P30)”

Which Python sorting algorithm is stable?

#Option
Asorted() uses an unstable sort
Blist.sort() uses TimSort, which is stable
CBoth are unstable
DStability depends on the data
EPython has no built-in sort

Correct: B (index 1)

Python’s built-in list.sort() and sorted() both use TimSort — a hybrid of merge sort and insertion sort. TimSort is stable: elements with equal keys maintain their original relative order. This is useful when sorting by multiple keys.

easy — 1 mark


What is the output?

from itertools import chain
a = [1, 2]
b = [3, 4]
c = [5]
result = list(chain(a, b, c))
print(result)
#Option
A[[1, 2], [3, 4], [5]]
B[1, 2, 3, 4, 5]
C[1, 2, 3, 4, 5, None]
D(1, 2, 3, 4, 5)
E12345

Correct: B (index 1)

chain concatenates iterables into a single iterator. chain(a, b, c) produces elements from a, then b, then c in sequence. Converting to a list gives [1, 2, 3, 4, 5]. Unlike flatMap, chain does not flatten nested structures.

easy — 1 mark


What is the output?

students = [("Alice", 88), ("Bob", 95), ("Charlie", 72), ("Diana", 95)]
students.sort(key=lambda s: (-s[1], s[0]))
print([s[0] for s in students])
#Option
A['Alice', 'Bob', 'Charlie', 'Diana']
B['Bob', 'Diana', 'Alice', 'Charlie']
C['Charlie', 'Alice', 'Bob', 'Diana']
D['Diana', 'Bob', 'Alice', 'Charlie']
E['Bob', 'Diana', 'Charlie', 'Alice']

Correct: B (index 1)

The sort key is (-s[1], s[0]) — descending score first, then ascending name alphabetically. Bob (95) and Diana (95) tie on score; “Bob” < “Diana” alphabetically, so Bob comes first. Then Alice (88), then Charlie (72). Result: ['Bob', 'Diana', 'Alice', 'Charlie'].

medium — 1 mark


What is the output?

from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
print(f"computing {n}", end=" ")
if n < 2: return n
return fib(n - 1) + fib(n - 2)
print(fib(5))
#Option
Acomputing 5 computing 4 computing 3 computing 2 computing 1 computing 0 5
B5
C55
DInfinite recursion
Ecomputing 5 5

Correct: A (index 0)

lru_cache memoizes results. fib(5) calls fib(4) and fib(3). Each call prints “computing” only on first invocation (cache miss). Subsequent calls to the same argument use the cached result. Total: computing 5, 4, 3, 2, 1, 0. The final result is 5. Without caching, fib(5) would compute 15 calls.

medium — 1 mark


What is the output?

try:
int("abc")
except ValueError as e:
print(type(e).__mro__)
#Option
A(<class 'ValueError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
B(<class 'Exception'>, <class 'ValueError'>, <class 'object'>)
CValueError
D<class 'ValueError'>
ECompiler error

Correct: A (index 0)

type(e).__mro__ prints the Method Resolution Order for ValueError. The MRO shows the inheritance chain: ValueError -> Exception -> BaseException -> object. This demonstrates that ValueError is a subclass of Exception, which is a subclass of BaseException.

medium — 1 mark


Which statement about the GIL is correct?

#Option
AThe GIL prevents all concurrency in Python
BThe GIL allows only one thread to execute Python bytecode at a time
CThe GIL only affects CPython, not other Python implementations
DThe GIL prevents I/O operations from running concurrently
EThe GIL was removed in Python 3.13

Correct: B (index 1)

The Global Interpreter Lock (GIL) ensures only one thread executes Python bytecode at a time, even on multi-core systems. This simplifies CPython’s memory management (reference counting) but limits CPU-bound parallelism. I/O operations release the GIL, so threads can overlap I/O. The GIL exists in CPython but not necessarily in other implementations (Jython, PyPy STM).

medium — 1 mark


What is the output?

import bisect
data = [10, 20, 30, 40, 50]
pos = bisect.bisect_left(data, 35)
print(pos)
#Option
A2
B3
C30
D35
E40

Correct: B (index 1)

bisect_left finds the insertion point for 35 in the sorted list. Since 35 is between 30 (index 2) and 40 (index 3), the leftmost insertion point is index 3. This means data.insert(3, 35) would maintain sorted order.

easy — 1 mark


What is the output?

from functools import reduce
nums = [1, 2, 3, 4, 5]
result = reduce(lambda acc, x: acc + x * x, nums, 0)
print(result)
#Option
A15
B55
C225
D0
E25

Correct: B (index 1)

reduce applies the lambda cumulatively: ((0 + 1^2) + 2^2) + 3^2) + 4^2) + 5^2 = 0 + 1 + 4 + 9 + 16 + 25 = 55. The initial value is 0. The lambda takes the accumulator and the current element, adding the square of each element.

easy — 1 mark


Click to reveal the answer key
QuestionAnswerQuestionAnswerQuestionAnswer
P1CP11AP21A
P2BP12CP22B
P3BP13AP23B
P4BP14BP24B
P5AP15CP25B
P6BP16BP26A
P7BP17EP27A
P8CP18BP28B
P9CP19BP29B
P10DP20AP30B

DifficultyCount
Easy13
Medium16
Hard1


  1. Trace code by hand. Python’s dynamic nature means subtle bugs (mutable defaults, variable scoping) require careful tracing.
  2. Know the data model. Understanding __dunder__ methods, the MRO, and the descriptor protocol is essential for advanced Python.
  3. Understand the “why”. Python’s design philosophy (EAFP over LBYL, duck typing, the GIL) has clear rationale. Understanding the motivation makes the language easier to master.
  4. Practise reading errors. Python tracebacks tell you exactly what went wrong — learn to read them efficiently.
  5. Retake after one week. Python’s flexibility means there are many subtle rules — spaced repetition is essential.

Last updated: 24 July 2026

Written by Wyatt. For questions or feedback, visit wyattau.com.