PCEP study materials
PCEP study resources

PCEP common mistakes

PCEP · Understand the traps and how to avoid them.

Mistake 1

Predicting that -7 // 2 is -3 and that -7 % 2 is -1, i.e. applying C/Java truncate-toward-zero rules to Python's // and %.

Most candidates arrive from languages where integer division truncates toward zero and the remainder takes the dividend's sign. Python's // floors toward negative infinity and % takes the divisor's sign, so answers only differ when a negative operand is involved - exactly the case the exam likes to test.

What to do instead

Re-derive with the identity a == (a // b) * b + (a % b) and check the sign of b first: -7 // 2 == -4, -7 % 2 == 1, 7 % -2 == -1, 7 // -2 == -4. Also recall // on floats returns a float (7.0 // 2 -> 3.0) and that /, // and % all raise ZeroDivisionError.

Mistake 2

Evaluating Boolean and arithmetic expressions with equal precedence and left-to-right grouping, e.g. reading 'not a == b' as '(not a) == b', '2 ** 3 ** 2' as 64, or assuming 'x or y' always evaluates to True/False.

English reading order and other languages' syntax suggest these groupings, but 'not' binds looser than comparisons, '**' is right-associative, comparisons chain, and 'and'/'or' short-circuit by returning one of their operands rather than a bool.

What to do instead

Memorise the ladder: ** (right-assoc), unary +/-, * / // %, + -, comparisons (chained, e.g. 5 > 3 > 1 is True but (5 > 3) > 1 is False), not, and, or. So 2 ** 3 ** 2 == 512 and not 1 == 2 is True. Track operand returns too: 0 or 'x' -> 'x', [] and 'x' -> [], 'a' and 'b' -> 'b'.

Mistake 3

Assuming the else clause of a for or while loop runs only when the body never executed, or that it still runs after a break.

The keyword 'else' suggests if/else semantics, and many candidates also confuse it with the try/except/else clause. Python's loop else is really a 'no break' clause.

What to do instead

Read loop else as 'executed only if the loop finished without hitting break'. It runs for an empty iterable (for x in []: ... else: runs), runs when a while condition becomes false, and is not skipped by continue. A break anywhere in the body skips it; use a found flag if the logic feels ambiguous.

Mistake 4

Miscounting how many times a loop body runs: treating a dedented statement as part of the body, or expecting range(5, 0) / range(0, 5, -1) to produce values.

Python marks blocks by indentation rather than braces, and editors re-indent on paste, so a total or print that sits at the header's column runs once, not per iteration. range() is also stop-exclusive and direction-sensitive, so a start/stop/step combination that 'looks ascending' yields an empty sequence.

What to do instead

Trace loops by writing the iteration number and the value of every variable in a column, and check each line's indentation against the colon-terminated header: the body is exactly the lines indented deeper. Remember range(a, b) stops before b, and a negative step needs start > stop (range(5, 0, -1) -> 5,4,3,2,1; range(0, 5, -1) -> empty).

Mistake 5

Treating b = a as a copy for lists and dicts, and deleting elements from a list with a for loop over that same list.

Assignment binds a name to an existing object, so a and b are two labels on one box and a mutation through either name shows up in both. A for loop walks a live index, so removing an element shifts the remaining ones left and the following element is skipped.

What to do instead

Draw both names pointing at one object; create independent copies with a[:], a.copy(), list(a), dict(d) or copy.deepcopy for nested data. To filter or delete, build a new list ([x for x in a if keep(x)]) or iterate over a copy (for x in a[:]), and remember that passing a list to a function passes the same object, so in-place changes are visible to the caller.

Mistake 6

Writing t = (5) and expecting a one-element tuple, s = {} and expecting an empty set, calling s[0] on a set, or assuming str.upper() / tuple item assignment mutate the object.

Parentheses only group an expression, {} is a dict literal because dict predates set literals, sets are unordered and unindexed, and strings and tuples are immutable, so their methods return new objects instead of changing one in place.

What to do instead

Use (5,) for a one-element tuple, set() for an empty set (or {1, 2} for a populated one), and iterate a set or use sorted(s)[0] rather than indexing it. Assign with s = s.upper(). Expect TypeError for s[0] = 'x' on a str or tuple, and remember a slice never raises for an out-of-range bound ('abc'[10:20] -> '') while an index does ('abc'[10] -> IndexError).

Mistake 7

Defining a function such as def add(item, bucket=[]) and being surprised that successive calls accumulate items, or assuming the caller's arguments are always protected from change.

Default expressions are evaluated once, when the def statement executes, so the same list object is reused for every call that omits the argument. Similarly, arguments are bound by object reference, so mutating a list or dict parameter is visible to the caller, while rebinding an immutable parameter is not.

What to do instead

Use an immutable sentinel and build inside the body: def add(item, bucket=None): if bucket is None: bucket = []. For the passing question, ask whether the function mutates the object (visible outside) or rebinds the name (invisible outside), and remember that reassigning a global inside a function without a global declaration creates a local, producing UnboundLocalError.

Mistake 8

Putting a broad except Exception (or a bare except) before a specific handler, assuming the exception name stays usable after the block, or assuming a finally block is skipped when the try block returns.

Handlers are tested top to bottom and the first match wins, so a broad clause makes later ones dead code; in Python 3 the 'as e' name is deleted when the except block ends; and finally is designed to run on every exit path, including return, break and an uncaught exception.

What to do instead

Order handlers from most specific to most general (ValueError before Exception), avoid bare except (it also catches KeyboardInterrupt and SystemExit), and copy the exception to another variable if you need it later. Remember try/except/else runs else only when no exception occurred, and that raising a built-in like ValueError('msg') is legal while raise 'msg' is a TypeError.

Mistake 9

Opening a file with mode 'w' to read data, forgetting that readlines() keeps the trailing newline, or writing an int directly to a file handle.

Mode 'w' silently truncates an existing file before any read or write, the default mode is 'r' so a missing file raises FileNotFoundError, read() and readlines() preserve '\n' characters, and write() accepts only str, so non-string arguments raise TypeError.

What to do instead

Choose the mode deliberately: 'r' read, 'w' truncate-and-write, 'a' append, 'x' create-or-fail. Use with open(path) as f: so the file closes even on error, compare lines with line.strip() or line.rstrip('\n'), and convert before writing (f.write(str(n) + '\n')). Remember a closed handle raises ValueError: I/O operation on closed file.

Mistake 10

Mixing the two import forms - writing import math and then calling sqrt(16), or from math import sqrt and then calling math.sqrt(16) - or expecting a module's top-level code not to run on import.

import math binds only the name math, so the function must be reached as math.sqrt; from math import sqrt binds the function name directly and leaves math undefined. Module-level statements execute once, the first time the module is imported in that process, and the result is cached in sys.modules.

What to do instead

Decide which name you created: after import math use math.sqrt; after from math import sqrt use sqrt. Avoid from module import * in exam code because later names can shadow earlier ones. If a script must both be importable and runnable, guard side effects with if name == 'main':, and watch for a local file whose name shadows a standard-library module.