Free PCAP-31-03 Full-Length Practice Exam: 40 Questions

Try 40 free PCAP-31-03 questions across the exam domains, with explanations, then continue with full IT Mastery practice.

This free full-length PCAP-31-03 practice exam includes 40 original IT Mastery questions across the exam domains.

These questions are for self-assessment. They are not official exam questions and do not imply affiliation with the exam sponsor.

Count note: this page uses the full-length practice count maintained in the Mastery exam catalog. Some certification vendors publish total questions, scored questions, duration, or unscored/pretest-item rules differently; always confirm exam-day rules with the sponsor.

Need concept review first? Read the PCAP-31-03 Cheat Sheet on Tech Exam Lexicon, then return here for timed mocks and full IT Mastery practice.

Open the matching IT Mastery practice page for timed mocks, topic drills, progress tracking, explanations, and full practice.

Try PCAP-31-03 on Web View full PCAP-31-03 practice page

Exam snapshot

  • Exam route: PCAP-31-03
  • Practice-set question count: 40
  • Time limit: 65 minutes
  • Practice style: mixed-domain diagnostic run with answer explanations

Full-length exam mix

DomainWeight
Section 1: Modules and Packages12%
Section 2: Exceptions14%
Section 3: Strings18%
Section 4: Object-Oriented Programming34%
Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)22%

Use this as one diagnostic run. IT Mastery gives you timed mocks, topic drills, analytics, code-reading practice where relevant, and full practice.

Practice questions

Questions 1-25

Question 1

Topic: Section 4: Object-Oriented Programming

In a fleet-tracking application, a developer defines these classes:

class Vehicle:
    pass

class Car(Vehicle):
    pass

class Bike(Vehicle):
    pass

Which TWO statements correctly identify the superclass/subclass relationships?

Options:

  • A. Bike is a subclass of Vehicle.

  • B. Bike is a superclass of Car.

  • C. Vehicle is a superclass of Car.

  • D. Vehicle is a subclass of Bike.

  • E. Car is a superclass of Vehicle.

  • F. Car inherits directly from Bike.

Correct answers: A and C

Explanation: In Python inheritance syntax, the class being created is written before the parentheses, and the parent class is written inside them. So Car(Vehicle) makes Car a subclass of Vehicle, and Bike(Vehicle) makes Bike a subclass of Vehicle.

The core rule is class Child(Parent):. The name before the parentheses is the new class, and the name inside the parentheses is the direct superclass, also called the base class. In this snippet, both Car and Bike are defined by inheriting from Vehicle, so Vehicle is the superclass for each of them.

  • class Car(Vehicle): means Car is a subclass of Vehicle.
  • class Bike(Vehicle): means Bike is a subclass of Vehicle.

A common mistake is to reverse the relationship or assume that two classes with the same parent inherit from each other.

  • Reversed relationship The option making Car the superclass of Vehicle flips child and parent.
  • Reversed again The option making Vehicle a subclass of Bike misreads the inheritance syntax.
  • Sibling confusion The option saying Car inherits from Bike is wrong because both inherit from Vehicle.
  • Wrong parent-child pair The option making Bike the superclass of Car treats sibling classes as if one inherited from the other.

Question 2

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A maintenance script already has this open text stream, and each option is evaluated independently:

stream = open("notes.txt", "rt")

From the current position, exactly 12 characters remain in the file, spread across two lines. The developer wants data to contain all remaining characters by using the read method. Which TWO statements satisfy the requirement?

Options:

  • A. data = stream.read(12)

  • B. data = stream.read()

  • C. data = stream.read(11)

  • D. data = stream.readline()

  • E. data = stream.read

  • F. data = stream.readall()

Correct answers: A and B

Explanation: The read method retrieves data from an opened readable stream. With no argument it returns all remaining content, and with an integer it returns up to that many characters, so read(12) also works here because the stem says exactly 12 characters remain.

read() is the standard file-object method for getting data from a readable stream. When called with no argument, it returns everything from the current cursor position to the end of the file. When called with an integer in text mode, it returns up to that many characters.

Because the stem states that exactly 12 characters remain, both of these return the full remaining contents:

  • stream.read()
  • stream.read(12)

A smaller size reads only part of the file. The fact that the remaining content spans two lines matters because readline() stops after the next line break, not after all remaining data. Also, stream.read without parentheses is just a method reference, not a read operation.

  • Too few characters the option using read(11) leaves one character unread.
  • One line only the option using readline() stops at the next newline, so it does not guarantee all remaining content.
  • Method reference the option using stream.read does not call the method.
  • Invalid method the option using readall() is not the normal file-object API in this PCAP context.

Question 3

Topic: Section 4: Object-Oriented Programming

A developer is reviewing this class design:

class Device:
    def status(self):
        return "device"

class Printer(Device):
    def status(self):
        return "printer"

class Scanner(Device):
    def status(self):
        return "scanner"

class AllInOne(Printer, Scanner):
    pass

The team says this diamond-shaped inheritance is harder to reason about than single inheritance. Which TWO statements correctly explain why?

Options:

  • A. A diamond hierarchy prevents subclasses from overriding inherited methods.

  • B. Python automatically calls both parent versions of status() for one method call.

  • C. You must know the method resolution order to predict which parent method is used.

  • D. The shared base class is reached through more than one inheritance path.

  • E. Method lookup becomes random when two parents share the same ancestor.

  • F. The Device class is copied into two separate embedded objects inside AllInOne.

Correct answers: C and D

Explanation: Diamond-shaped inheritance makes one class reachable through multiple parent paths. That means the same method name may be available from more than one place, so you must trace Python’s method resolution order to know what will actually run.

The core issue is multiple inheritance with a shared ancestor. In this example, AllInOne inherits from both Printer and Scanner, and both of those inherit from Device. That creates two paths back to the same base class, which makes method lookup harder to follow than a single straight inheritance chain.

  • More than one parent can define the same method name.
  • Python resolves that name using a deterministic method resolution order (MRO).
  • The shared ancestor is still part of the lookup chain through multiple routes.

For this hierarchy, reasoning about behavior means tracing the MRO, not just looking at one direct parent. The difficulty is not randomness; it is the extra lookup path and competing method definitions.

  • Automatic dual calls is wrong because a normal method call resolves to one method through MRO; Python does not call both parent methods by default.
  • Copied base object is wrong because inheritance defines lookup relationships, not duplicated embedded Device objects.
  • No overriding allowed is wrong because subclasses in Python can still override methods in multiple-inheritance hierarchies.
  • Random lookup is wrong because Python uses a deterministic resolution order, even in a diamond pattern.

Question 4

Topic: Section 3: Strings

A file upload utility stores the uploaded file name in filename and the user’s note in note. Both variables are strings. The utility may continue only when .exe is absent from filename and # is absent from note. Which TWO conditions correctly use not in for these checks?

Options:

  • A. filename not in '.exe'

  • B. '#' not in note

  • C. note not in '#'

  • D. note != '#'

  • E. '.exe' not in filename

  • F. '.exe' != filename

Correct answers: B and E

Explanation: not in is the proper membership operator for checking that a character or substring does not occur anywhere in a string. The valid conditions put the forbidden text on the left and the string being searched on the right, so the result is True only when that text is absent.

In a string membership test, the left side is the character or substring you want to find, and the right side is the string you want to search. The expression 'text' not in some_string evaluates to True only when that character sequence does not appear anywhere inside some_string.

That is why '.exe' not in filename and '#' not in note are correct absence checks. Reversing the order changes the meaning, because Python would then ask whether the entire variable value appears inside the short literal. Using != is also different: it compares whole strings for equality and does not search within them.

For substring or character absence in a string, use not in with the searched text first and the target string second.

  • The reversed membership check with filename not in '.exe' tests whether the whole filename appears inside the literal .exe.
  • The check note != '#' only asks whether the entire note is exactly #, not whether # appears inside it.
  • The check '.exe' != filename compares full strings for inequality and does not perform a substring search.
  • The reversed form note not in '#' searches in the wrong direction and does not test whether # is absent from note.

Question 5

Topic: Section 2: Exceptions

A developer expects bad number when invalid input is processed, but that message never appears; for "x" the program prints generic failure instead.

values = ["4", "x"]

for v in values:
    try:
        print(12 / int(v))
    except Exception:
        print("generic failure")
    except ValueError:
        print("bad number")

What is the best fix?

Options:

  • A. Place except ValueError before except Exception.

  • B. Move int(v) outside the try block.

  • C. Use raise inside except Exception to let except ValueError run.

  • D. Replace except Exception with except BaseException.

Best answer: A

Explanation: Python checks except clauses from top to bottom. Here, int("x") raises ValueError, but except Exception catches it first because ValueError inherits from Exception. The specific handler must come before the general one.

The core rule is exception-handler ordering: Python executes the first except clause whose type matches the raised exception. In this code, int("x") raises ValueError. Because ValueError is part of the Exception hierarchy, the earlier except Exception clause already matches, so the later except ValueError clause is effectively unreachable for that error.

  • Put more specific exception classes first.
  • Put broader fallback handlers later.
  • Use a generic except Exception only as a last resort.

A common mistake is assuming Python will keep checking later handlers after entering a broad one, but it does not.

  • Re-raise idea fails because raising from the broad handler sends the exception outward; it does not continue to a later sibling except.
  • Broader catch with BaseException makes the problem worse by catching even more cases before the specific handler.
  • Move conversion out would prevent this try block from handling the ValueError at all.

Question 6

Topic: Section 3: Strings

A developer defines a class so that printing an object returns the text after the last :: in an event code. What is printed by this program?

class Event:
    def __init__(self, code):
        self.code = code

    def __str__(self):
        pos = self.code.rfind("::")
        return self.code[pos + 2:]

print(Event("SYS::IO::WARN"))

Options:

  • A. WARN

  • B. IO::WARN

  • C. SYS::IO::WARN

  • D. SYS::IO

Best answer: A

Explanation: rfind() searches for the last occurrence of a substring and returns its starting index. In SYS::IO::WARN, the final :: is immediately before WARN, so slicing from two characters after that index returns WARN.

The key concept is str.rfind(sub): it returns the highest index where sub begins. In the __str__() method, self.code.rfind("::") finds the second ::, not the first one. Since the separator has length 2, the slice starts at pos + 2, which skips over that final separator and keeps only the text that follows it.

  • SYS::IO::WARN contains :: twice.
  • rfind("::") points to the last one.
  • self.code[pos + 2:] returns the trailing part of the string.

A common mistake is to think of rfind() as if it behaved like find(), which would lead to the substring after the first separator instead.

  • The option showing IO::WARN assumes the first :: was used instead of the last one.
  • The option showing SYS::IO would come from slicing before the last separator, not after it.
  • The option showing the full code ignores that __str__() returns only a slice of self.code.

Question 7

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A maintenance script has two separate tasks: read log.txt as UTF-8 text and count the substring ERROR, and load packet.bin as mutable raw data so one byte can be changed before saving. Which TWO approaches use the correct type for the file mode and requirement?

Options:

  • A. Open packet.bin with 'rt', store f.read() in buf, then assign buf[0] = '\xff'.

  • B. Open log.txt with 'rt', encoding='utf-8', build bytearray(f.read()), then call count('ERROR').

  • C. Open log.txt with 'rt', encoding='utf-8', keep f.read() as str, then call count('ERROR').

  • D. Open log.txt with 'rb', keep f.read() as bytes, then call count('ERROR').

  • E. Open packet.bin with 'rb', convert f.read() to bytearray, then assign buf[0] = 255.

  • F. Open packet.bin with 'rb', keep f.read() as bytes, then assign buf[0] = 255.

Correct answers: C and E

Explanation: Use str for text-mode, character-based work such as searching a UTF-8 log. Use bytearray for binary data that must be modified, because binary reads produce immutable bytes unless you convert them.

In Python, text mode ('rt') decodes file content into a str, so normal string operations such as count('ERROR') apply directly to the data. Binary mode ('rb') reads raw bytes and returns a bytes object. If you need to change individual byte values, convert that bytes object to bytearray, because bytearray is mutable while bytes is not.

Applied here, the log-processing task is text-oriented, so a str is the correct choice. The packet task is raw binary and requires mutation, so bytearray is the correct buffer type. A common near-miss is reading binary data correctly but forgetting that bytes cannot be changed in place.

  • The binary-read log option is wrong because bytes is not the right type for a text search using the string 'ERROR'.
  • The text-read packet option is wrong because text mode produces decoded characters, not a raw byte buffer, and str is immutable.
  • The binary-read packet option that keeps bytes is wrong because indexed assignment like buf[0] = 255 is not allowed on bytes.
  • The text-read option that builds bytearray(f.read()) is wrong because f.read() returns a str in text mode, and that conversion is not the right fit for a text-processing task.

Question 8

Topic: Section 1: Modules and Packages

A developer is exploring an unfamiliar standard-library module and already has this code:

import random
rng = random.Random(0)

They want to use dir() to inspect the names exposed by the imported random module or by the existing rng object. Which two statements correctly do that? Select TWO.

Options:

  • A. print(random.dir())

  • B. print(dir("rng"))

  • C. print(dir(random()))

  • D. print(dir(rng))

  • E. print(rng.dir())

  • F. print(dir(random))

Correct answers: D and F

Explanation: dir() is a built-in function, so you use it by passing the target module or object as an argument. Passing random or rng directly is correct for inspecting exposed names in this scenario.

In Python, the correct form is dir(target), where target can be a module, class, or instance. When the target is the imported random module, dir(random) returns a list of names available from that module. When the target is the rng object, dir(rng) returns the attributes and methods visible on that instance.

A common mistake is to treat dir like a method, but it is not written as random.dir() or rng.dir(). Another mistake is inspecting the wrong thing: random() is invalid here because random refers to the module object, and "rng" is only a string literal, not the existing Random instance. The key rule is simple: pass the actual module or object directly to dir().

  • The option using random.dir() fails because dir is not a module method.
  • The option using rng.dir() fails because instances do not use dir as an attribute-inspection method.
  • The option using random() fails because random in the snippet is the imported module, not a callable.
  • The option using "rng" inspects a string object, not the existing rng variable from the code.

Question 9

Topic: Section 3: Strings

In Python 3, what is the output of this code?

s = "€"

try:
    b = s.encode("utf-8")
    if len(s) != len(b):
        raise ValueError(len(s), len(b))
except ValueError as e:
    print(e.args[0], e.args[1])
finally:
    print(type(b).__name__)

Options:

  • A. 3 3, then bytes

  • B. 1 3, then str

  • C. 1 1, then bytes

  • D. 1 3, then bytes

Best answer: D

Explanation: len(s) counts characters in the Unicode string, while len(s.encode("utf-8")) counts UTF-8 bytes. For , those values are 1 and 3, so the ValueError handler prints 1 3, and finally prints bytes.

A Python str stores Unicode text, not raw UTF-8 bytes. UTF-8 is a variable-length encoding, so one Unicode character may take more than one byte when encoded.

  • s contains one character:
  • b = s.encode("utf-8") creates a bytes object
  • uses 3 bytes in UTF-8, so len(s) is 1 and len(b) is 3
  • Because the lengths differ, ValueError(1, 3) is raised and caught
  • The handler prints the exception arguments as 1 3
  • The finally block always runs and prints bytes

The closest mistake is assuming one character always equals one byte, which is not true for many non-ASCII Unicode characters.

  • One-byte assumption fails because is one character but not one UTF-8 byte.
  • Both lengths as 3 fails because only the encoded bytes object has length 3; the original str still has length 1.
  • Wrong result type fails because encode() returns bytes, not str.

Question 10

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A developer needs to read the first line from an existing text file and append its uppercase version to the end of the same file.

data.txt initially contains:

one
two

The current code does not produce the required final contents.

with open("data.txt", "a") as f:
    first = f.readline().strip()
    f.write(first.upper() + "\n")

The required final contents are:

one
two
ONE

Which replacement is the best fix?

Options:

  • A. Open with r+ and keep the rest of the code unchanged.

  • B. Open with w+ and keep the rest of the code unchanged.

  • C. Open with a+ and add f.seek(0) before readline().

  • D. Open with a+ and keep the rest of the code unchanged.

Best answer: C

Explanation: The code must both read existing content and append new content without deleting the file. a+ allows both operations, but it opens the file at the end, so f.seek(0) is required before reading the first line.

This is a file-mode and cursor-position problem. The file must stay intact, the first line must be read, and the uppercase version must be added at the end. a+ is the right mode because it allows reading and writing without truncating the file, and writes in append mode go to the end of the file.

  • Open with a+.
  • Call f.seek(0) to move from EOF to the start.
  • Read the first line with readline().
  • Write the uppercase text; append mode places it at the end.

The closest wrong choice is r+: it allows reading and writing, but after reading the first line the next write occurs at the current position, so it overwrites existing content instead of appending.

  • r+ is tempting because it supports reading and writing, but the write starts at the current cursor position rather than appending.
  • w+ fails because opening the file in that mode truncates it immediately.
  • a+ without seek(0) starts reading at EOF, so readline() returns an empty string.

Question 11

Topic: Section 4: Object-Oriented Programming

A developer is testing a method that should update an instance attribute. What happens when this Python 3 code runs?

class Counter:
    def __init__(self, start):
        self.value = start

    def add(step):
        self.value += step

c = Counter(10)
c.add(5)
print(c.value)

Options:

  • A. It raises TypeError on c.add(5)

  • B. It prints 15

  • C. It prints 10

  • D. It raises NameError on self.value += step

Best answer: A

Explanation: Instance methods must declare self as the first parameter. When c.add(5) is called, Python automatically passes the instance and then 5, so add(step) receives too many arguments and raises TypeError before any update occurs.

In Python, a function accessed through an instance becomes a bound method. That means the instance is supplied automatically as the first argument during the call. Here, c.add(5) effectively tries to call add(c, 5).

But the method was defined as def add(step):, so it accepts only one positional argument. Because the call provides two, Python raises TypeError immediately. The line self.value += step is never executed, so print(c.value) is never reached. If the method were defined as def add(self, step):, then the instance would bind to self, 5 would bind to step, and the value would become 15.

  • The option claiming it prints 15 assumes a correct instance-method signature; the missing self prevents the method call from succeeding.
  • The option claiming it prints 10 is wrong because the program stops at c.add(5) and never reaches print(c.value).
  • The option claiming NameError overlooks that Python detects the wrong argument count first, so the method body is not executed.

Question 12

Topic: Section 1: Modules and Packages

A developer wants this snippet to print 6.0, but it raises an error:

from math import hypot, sqrt

result = math.hypot(6, 8) - sqrt(16)
print(result)

Which change is the best fix?

Options:

  • A. Change only the first line to import math

  • B. Change the calculation to result = hypot([6, 8]) - sqrt(16)

  • C. Change the calculation to result = math.hypot(6, 8) - math.sqrt(16)

  • D. Change the calculation to result = hypot(6, 8) - sqrt(16)

Best answer: D

Explanation: from math import hypot, sqrt imports those function names directly into the current namespace. In this snippet, sqrt(16) is already correct, but math.hypot(6, 8) is not because no math module name was imported.

Python supports two common import styles for module functions. With import math, you call functions through the module name, such as math.sqrt(16). With from math import hypot, sqrt, you call the imported names directly, such as hypot(6, 8) and sqrt(16).

Here, the code uses the second style, so only the hypot call is wrong. After the fix, the expression evaluates normally: hypot(6, 8) returns 10.0, sqrt(16) returns 4.0, and the final result is 6.0.

The key takeaway is to match the function call style to the way the module was imported.

  • Changing only the import to import math leaves sqrt(16) undefined, because sqrt is no longer imported directly.
  • Prefixing both calls with math. still fails, because the name math does not exist after from math import hypot, sqrt.
  • Passing [6, 8] to hypot uses the wrong signature; hypot expects numeric arguments, not a single list.

Question 13

Topic: Section 1: Modules and Packages

A team stores constants and a helper class in settings.py:

# settings.py
timeout = 30
_retry_limit = 5
__trace = True

class Worker:
    __mode = "safe"

Another file executes:

from settings import *
import settings

Which statement about the module variables is correct?

Options:

  • A. __trace is name-mangled to _settings__trace because double underscores always mangle names.

  • B. Only _retry_limit is skipped by wildcard import; __trace is imported because it uses two underscores.

  • C. timeout is public; _retry_limit and __trace are non-public by convention and are skipped by wildcard import.

  • D. _retry_limit becomes a class variable of Worker because it is defined before the class.

Best answer: C

Explanation: At module level, leading underscores mark non-public names by convention. That means timeout is public, while _retry_limit and __trace are skipped by from settings import *; double underscores here do not create class-style name mangling.

Python modules use naming conventions, not true access control, to signal public versus non-public names. A module variable with no leading underscore, such as timeout, is public. Any module name beginning with _, including _retry_limit and __trace, is considered non-public by convention.

This convention also affects from module import *: wildcard import omits names that start with an underscore unless the module explicitly defines __all__. The key trap is confusing module names with class attributes. Double leading underscores trigger name mangling only inside class definitions, such as Worker.__mode, not for module-level variables like __trace.

So __trace remains a normal name in the module namespace, but wildcard import still skips it because it starts with _.

  • Name mangling mix-up fails because module-level __trace is not name-mangled; name mangling applies to class attributes.
  • Class variable confusion fails because a module variable does not become part of a class just because it appears before the class definition.
  • Double underscore import myth fails because wildcard import skips any name starting with _, including names starting with __.

Question 14

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

Assume data.txt already exists, so open("data.txt", "r") succeeds. A developer wants to write text but opened the file in read mode. Which statement about this code is correct?

from io import UnsupportedOperation

try:
    fh = open("data.txt", "r")
    fh.write("X")
except UnsupportedOperation as ex:
    print("mode problem:", ex.args[0])
except (OSError, ValueError):
    print("fallback")
else:
    print("write ok")
finally:
    print("finished")
    fh.close()

Options:

  • A. The UnsupportedOperation handler runs, else is skipped, and finally still runs.

  • B. else runs because opening the file succeeded before the write() call.

  • C. No exception is raised because 'r' mode allows both reading and writing.

  • D. The grouped (OSError, ValueError) handler runs first because UnsupportedOperation is also an OSError.

Best answer: A

Explanation: This is a file mode mismatch: write() is attempted on a handle opened with 'r', so Python raises io.UnsupportedOperation. The specific except UnsupportedOperation as ex block handles it first, else does not run, and finally runs anyway.

File modes control which operations are allowed on a handle. Mode 'r' opens the file for reading only, so calling write() on that handle raises io.UnsupportedOperation. In this code, exception matching is checked top to bottom, and the specific except UnsupportedOperation as ex block is reached before the broader grouped handler.

A quick way to reason about the flow is:

  • open("data.txt", "r") succeeds.
  • fh.write("X") fails because the handle is not writable.
  • The first matching except runs.
  • else is skipped because an exception occurred.
  • finally runs whether or not the exception was handled.

The closest distractor is the grouped handler, but broader handlers do not run when an earlier, more specific handler already matched.

  • Broader handler first fails because except blocks are checked in order, so the specific UnsupportedOperation block catches the error before the grouped one.
  • else after open fails because else runs only when the entire try block completes without any exception.
  • Read mode writes fails because 'r' is read-only; writing requires a writable mode such as 'w', 'a', or 'r+'.

Question 15

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A developer is cleaning usernames before saving a report. The script should remove surrounding spaces and convert each name to uppercase, but instead of ['ALICE', 'BOB', 'CAROL'] it prints a list of built-in method objects.

names = [" alice ", " Bob ", " carol "]
cleaned = list(map(lambda s: s.strip().upper, names))
print(cleaned)

What is the best fix?

Options:

  • A. Call upper() inside the lambda.

  • B. Convert names to a tuple first.

  • C. Replace map() with filter().

  • D. Remove list() from the assignment.

Best answer: A

Explanation: map() is fine here; the problem is the lambda body. s.strip().upper refers to the string method object, while s.strip().upper() actually calls the method and returns the uppercase result.

map() applies the lambda to each item in names, so the key question is what the lambda returns. In this code, s.strip().upper first creates the stripped string, then accesses its upper method without calling it. That means each mapped result is a method object, not transformed text.

The fix is to call the method:

cleaned = list(map(lambda s: s.strip().upper(), names))

Now each element is stripped and converted to uppercase before being collected into a list. map() is the right tool because every input element must be transformed, not filtered out or left as-is.

  • Replacing map() with filter() fails because filter() keeps or discards items; it does not transform each string.
  • Converting names to a tuple does not change what the lambda returns.
  • Removing list() would only leave a map iterator; the lambda would still produce method objects.

Question 16

Topic: Section 3: Strings

A developer scans a short status tag one character at a time and builds a new string. What is printed by this code?

tag = "Py 3!"
result = ""

for ch in tag:
    if ch.isalpha():
        result += ch.lower()
    elif ch == " ":
        result += "-"
    else:
        result += "*"

print(result)

Options:

  • A. py-**

  • B. py**

  • C. py-3!

  • D. Py-**

Best answer: A

Explanation: The for ch in tag loop iterates through the string one character at a time, including the space, digit, and punctuation. Letters are lowercased, the space becomes -, and all other characters become *, so the final result is py-**.

A Python string is iterable, so for ch in tag visits each character in order: P, y, space, 3, and !. For alphabetic characters, ch.isalpha() is True, so ch.lower() adds p and y to result. When the loop reaches the space, the second branch matches and adds -. The remaining characters, 3 and !, are neither alphabetic nor spaces, so they both go to the else branch and become *. After all characters are processed, print(result) outputs py-**.

The key takeaway is that string iteration handles every character individually unless the code explicitly skips one.

  • The output keeping uppercase P ignores that alphabetic characters are passed through .lower().
  • The output without - treats the space as if it were skipped, but the code explicitly replaces it.
  • The output preserving 3! ignores that nonletters that are not spaces fall into the else branch.

Question 17

Topic: Section 4: Object-Oriented Programming

A transport application already defines Vehicle, LandVehicle(Vehicle), and WaterVehicle(Vehicle). A new class, AmphibiousVehicle, must inherit behavior from both LandVehicle and WaterVehicle, creating a diamond-shaped hierarchy through Vehicle. Which definition matches this requirement?

Options:

  • A. class AmphibiousVehicle(Vehicle): pass

  • B. class AmphibiousVehicle(LandVehicle): pass

  • C. class AmphibiousVehicle(WaterVehicle): pass

  • D. class AmphibiousVehicle(LandVehicle, WaterVehicle): pass

Best answer: D

Explanation: The requirement says the new class must inherit from two existing classes, not just one. In Python, that means multiple inheritance with both base classes listed in the class header.

This hierarchy requires multiple inheritance because AmphibiousVehicle must be a subclass of both LandVehicle and WaterVehicle. Since those two classes already inherit from Vehicle, adding AmphibiousVehicle above them forms the classic diamond shape.

In Python, multiple inheritance is declared by listing more than one base class in the class definition:

  • LandVehicle is one direct base.
  • WaterVehicle is the second direct base.
  • Vehicle remains an indirect base through both paths.

Using only one parent class would create single inheritance and would not match the stated hierarchy. The key takeaway is that one child class with two direct parents requires multiple inheritance.

  • Inheriting only from Vehicle skips the requirement to derive from both specialized classes.
  • Inheriting only from LandVehicle is still single inheritance, so the water-side parent is missing.
  • Inheriting only from WaterVehicle has the same problem in reverse.

Question 18

Topic: Section 3: Strings

A utility receives import targets such as pkg.tools.reader and reader. It must locate the last . so it can separate the package path from the final module name, and it must return -1 when no dot exists.

name = target
cut = ???

Which replacement should be used?

Options:

  • A. name.index(".")

  • B. name.rfind(".")

  • C. name.find(".")

  • D. name.rindex(".")

Best answer: B

Explanation: Use rfind() when you need the last occurrence of a substring and a non-exception result for “not found.” That matches import targets that may be fully qualified like pkg.tools.reader or standalone like reader.

The key concept is that .rfind() searches for the last occurrence of a substring and returns its index, or -1 if the substring is absent. In a module/package scenario, the last dot separates the package path from the final module name, so searching from the right is the correct behavior.

  • pkg.tools.reader.rfind(.) points to the dot before reader.
  • reader.rfind(.) returns -1.

That makes .rfind() a safe choice when some import targets are not package-qualified. The closest distractor is .rindex(), which also finds the last occurrence but raises ValueError when the dot is missing.

  • The option using find() fails because it returns the first dot, not the last one.
  • The option using index() fails because it searches from the left and raises ValueError if no dot exists.
  • The option using rindex() finds the last dot, but it still raises ValueError when the string has no dot.

Question 19

Topic: Section 4: Object-Oriented Programming

A developer placed this class in a package module and imports it elsewhere.

# app/models.py
class User:
    def __init__(self, name, level):
        name = name
        level = level
# main.py
from app.models import User

u = User("Ana", 3)
print(hasattr(u, "name"), hasattr(u, "level"))

Which replacement for __init__() correctly binds both constructor arguments to instance variables?

Options:

  • A. def __init__(self, name, level): self.name = name; self.level = level

  • B. def __init__(name, level): self.name = name; self.level = level

  • C. def __init__(self, name, level): User.name = name; User.level = level

  • D. def __init__(self, name, level): name = self.name; level = self.level

Best answer: A

Explanation: Constructor parameters are local variables until they are attached to the object. Using self.name = name and self.level = level stores the values on each User instance.

In Python, __init__() receives the new object as self. Parameters such as name and level exist only inside that method unless you assign them to attributes on self. That is why self.name = name and self.level = level are the correct bindings for instance variables.

The import statement in main.py is just scenario context; it does not change how object initialization works. The original code uses name = name and level = level, which only refers to local names and creates no attributes on the object. Assigning through User.name would create class attributes shared by instances, not per-instance state. The key idea is that instance data must be stored through self.

  • The option without self as the first parameter is invalid for normal instance initialization.
  • The option assigning through User.name and User.level creates class variables, not instance variables.
  • The option reversing the assignment tries to read self.name and self.level before they exist.

Question 20

Topic: Section 4: Object-Oriented Programming

A developer wants print(t) to display Ticket 17 for Ana, but this code prints the default object representation instead.

class Ticket:
    def __init__(self, number, owner):
        self.number = number
        self.owner = owner

    def __str_(self):
        return f"Ticket {self.number} for {self.owner}"

t = Ticket(17, "Ana")
print(t)

Which change is the best fix?

Options:

  • A. Change the return line to use Ticket.number and Ticket.owner.

  • B. Change the method to def __str__(self, extra):.

  • C. Replace print(t) with print(t.number, t.owner).

  • D. Rename __str_ to __str__ and keep the current body.

Best answer: D

Explanation: print(obj) internally uses str(obj), which looks for a method named exactly __str__. Because the class defines __str_ instead, Python falls back to the default object text inherited from object.

This is a special-method lookup problem. In Python, print(t) calls str(t), and str(t) looks for a method named exactly __str__ with two trailing underscores. A misspelled name like __str_ is treated as an ordinary method, so it is ignored during string conversion.

The fix is to rename the method header to the exact special name:

  • def __str__(self):
  • return the desired string

After that, print(t) will use the returned text, such as Ticket 17 for Ana. Changing the print() call may display some data, but it does not correct the object’s string representation behavior.

  • Extra parameter fails because __str__ must be callable for the instance without an added argument.
  • Class attributes fail because number and owner were stored on each instance, not on the Ticket class.
  • Changing print() bypasses the real issue and does not define how the object should convert to a string.

Question 21

Topic: Section 4: Object-Oriented Programming

A developer is checking which members belong to the class itself in a small reporting app.

class Report:
    category = "daily"
    __limit = 3

    def __init__(self, name):
        self.name = name

    def show(self):
        return f"{self.name}:{Report.category}"

r = Report("sales")

Which statement about Report.__dict__ is correct?

Options:

  • A. It contains name because self.name is assigned in __init__.

  • B. It stores the private attribute under the key __limit.

  • C. It contains category, _Report__limit, __init__, and show.

  • D. The value of Report.__dict__['show'] is the bound method from r.show.

Best answer: C

Explanation: Report.__dict__ represents the class namespace, so it includes class-level attributes and methods defined in the class body. The private attribute is name-mangled to _Report__limit, while name belongs to the instance r, not to the class.

A class __dict__ shows names stored on the class itself. In this example, category is a class variable, and __init__ plus show are entries created by method definitions inside the class body. The attribute __limit starts with two underscores, so Python applies name mangling and stores it under _Report__limit in Report.__dict__.

By contrast, self.name = name creates an instance attribute during object initialization. That means name is found in r.__dict__, not in Report.__dict__. Also, the class dictionary stores the function object for show; it becomes a bound method only when accessed through an instance such as r.show.

So, when inspecting a class __dict__, look for class variables, methods, and mangled private names, not per-instance data.

  • The option claiming name appears in Report.__dict__ confuses an instance attribute with a class attribute.
  • The option using __limit unchanged ignores Python name mangling for double-underscore class names.
  • The option treating show as already bound misses that binding happens when the function is accessed through r.

Question 22

Topic: Section 4: Object-Oriented Programming

Given these files, what is printed when main.py is run?

# tools.py
class Hammer:
    pass

# main.py
from tools import Hammer

class Nail:
    pass

print(Hammer.__module__)
print(Nail.__module__)

Options:

  • A. tools, then main

  • B. tools, then __main__

  • C. __main__, then __main__

  • D. Hammer, then Nail

Best answer: B

Explanation: A class’s __module__ attribute stores the name of the module where that class was defined. Importing a class into another file does not change that value, and a file run directly uses the module name __main__.

The key concept is that ClassName.__module__ identifies the defining module, not the file currently using the class. Hammer is defined in tools.py, so Hammer.__module__ is tools even after from tools import Hammer. Nail is defined inside main.py, and because main.py is executed directly as the script, that module’s runtime name is __main__.

So the output is:

tools
__main__

The closest mistake is using main for Nail, but that would apply only if the file were imported as a module rather than run as the main script.

  • The option using tools, then main misses that a directly executed script has module name __main__.
  • The option using __main__ for both lines incorrectly assumes importing a class changes its defining module.
  • The option using Hammer, then Nail confuses __module__ with class names such as __name__.

Question 23

Topic: Section 4: Object-Oriented Programming

A developer is checking method lookup in a diamond-shaped inheritance hierarchy. What is printed by this Python 3 code?

class Top:
    def name(self):
        return "Top"

class Left(Top):
    def show(self):
        return self.name()

class Right(Top):
    def name(self):
        return "Right"

class Bottom(Left, Right):
    pass

print(Bottom().show())

Options:

  • A. Left

  • B. AttributeError is raised

  • C. Top

  • D. Right

Best answer: D

Explanation: The output is Right. Python uses method lookup based on the instance’s method resolution order (MRO), not only the class where the current method was found. After show() is taken from Left, self.name() is searched on the Bottom instance through Left, Right, and then Top.

Python resolves methods in multiple inheritance by following the method resolution order (MRO). For Bottom, that order is Bottom, Left, Right, Top, then object. The call to show() finds Left.show, but inside that method, self still refers to the Bottom instance. So self.name() is looked up using Bottom’s MRO as well. Left does not define name(), so Python continues to Right, finds Right.name(), and returns Right.

The key takeaway is that method lookup continues from the instance’s class hierarchy, not from only the class where the current method was found.

  • The option saying Top ignores that Right.name() appears earlier than Top.name() in Bottom’s lookup path.
  • The option saying Left confuses the class that provides show() with the class that provides name().
  • The exception option fails because a name() method does exist in the inheritance hierarchy, so lookup succeeds.

Question 24

Topic: Section 4: Object-Oriented Programming

A teammate is filling in # TODO in the class below. The method must be an instance method, follow the usual self convention, and let the else clause run.

class User:
    def __init__(self, name):
        self.name = name

    # TODO

u = User("Ana")
try:
    print(u.label())
except TypeError as e:
    print(type(e).__name__, e.args[0])
else:
    print("ok")
finally:
    print("done")

Which replacement is best?

Options:

  • A. def label(self, value): return self.name

  • B. def label(): return self.name

  • C. def label(self): return self.name

  • D. def label(obj): return obj.name

Best answer: C

Explanation: Instance methods receive the instance automatically when called through an object, so the method must accept that first argument. Using self as that first parameter is the normal convention, and this version allows u.label() to succeed, so else executes.

When Python evaluates u.label(), it creates a bound method call and passes u as the first argument automatically. For a normal instance method, the first parameter should therefore represent the instance, and by convention that parameter is named self.

If the method has no first parameter, the call gets one argument too many and raises TypeError. If it requires an extra parameter, calling u.label() without that value also raises TypeError. A name like obj would technically work, but it does not match the stated requirement to use the usual self convention.

So the best replacement is the one that both works at runtime and declares the instance parameter as self.

  • The option with no parameters fails because u.label() still passes the instance, causing a TypeError.
  • The option using obj would run, but it does not satisfy the requested self naming convention.
  • The option requiring value fails because the call provides only the bound instance, not a second argument.

Question 25

Topic: Section 3: Strings

A developer needs to send Python text through a byte-only channel and later rebuild the same value. The text may contain non-ASCII characters.

text = "Δx = café"
packet = text.encode("ascii")
restored = packet.decode("utf-8")
print(restored == text)

The program currently fails. Which change is the best fix?

Options:

  • A. Replace text.encode("ascii") with text.decode("utf-8").

  • B. Replace restored = packet.decode("utf-8") with restored = str(packet).

  • C. Replace text.encode("ascii") with ascii(text).encode("utf-8").

  • D. Replace text.encode("ascii") with text.encode("utf-8").

Best answer: D

Explanation: In Python, str values are Unicode text, not raw bytes. ASCII cannot represent characters like Δ or é, but UTF-8 can, so using UTF-8 for both encoding and decoding correctly round-trips the string.

The core concept is the difference between text and encodings. In Python 3, text is a Unicode str, so it already represents characters such as Δ and é. ASCII is a limited encoding that supports only 128 basic characters, so trying to encode this string as ASCII raises UnicodeEncodeError.

UTF-8 is a byte encoding for Unicode text. The correct flow is:

  • start with Unicode text in a str
  • convert it to bytes with encode("utf-8")
  • convert the bytes back with decode("utf-8")

That preserves the original content exactly. By contrast, ascii() creates an escaped representation, and str(packet) creates a printable bytes representation, not the decoded text.

  • Using ascii(text) fails because it turns non-ASCII characters into escape sequences, changing the original content.
  • Calling decode() on text fails because decode() is for bytes, while text is already a Unicode str.
  • Converting packet with str(packet) fails because it produces a representation like b'...', not the recovered text.

Questions 26-40

Question 26

Topic: Section 1: Modules and Packages

A developer replaces import helpers with from helpers import * in main.py.

Assume helpers.py is on sys.path and does not define __all__.

# helpers.py
value = 10
_hidden = 99

def show():
    return value

Which statement is correct after from helpers import * runs in main.py?

Options:

  • A. In main.py, value and show become local names, increasing collision risk; _hidden and helpers do not.

  • B. In main.py, public names are imported only when first used.

  • C. In main.py, value, _hidden, show, and helpers all become local names.

  • D. In main.py, only helpers becomes a local name, so collisions are avoided.

Best answer: A

Explanation: A star import adds public names from the module directly to the current namespace instead of creating a module name like helpers. Without __all__, names starting with _ are excluded, so this form is convenient but increases namespace pollution and naming-collision risk.

The key concept is that from module import * changes the caller’s namespace directly. In main.py, the public variable value and the function show become ordinary names available without qualification. That is the main risk: they can overwrite existing names in main.py, or later assignments in main.py can hide them.

Because helpers.py does not define __all__, Python uses the default rule for star import and skips names that start with _, so _hidden is not imported. Also, this import form does not create the name helpers; that happens with import helpers, not with from helpers import *.

A close distractor confuses star import with regular module import, but those two forms affect the namespace very differently.

  • The option claiming only helpers is imported confuses from helpers import * with import helpers.
  • The option claiming _hidden is also imported ignores the default rule that skips leading-underscore names when __all__ is absent.
  • The option claiming names are imported only when first used is wrong because the namespace is populated when the import statement executes.

Question 27

Topic: Section 2: Exceptions

An engineer uses a small trace function to verify cleanup behavior. What is printed by this code?

def trace(n):
    try:
        print("T", end="")
        10 // n
    except ZeroDivisionError:
        print("E", end="")
    else:
        print("L", end="")
    finally:
        print("F", end="")

for value in [2, 0]:
    trace(value)

print("!", end="")

Options:

  • A. TEFTLF!

  • B. TLFTEF!

  • C. TLTEFF!

  • D. TLFTE!

Best answer: B

Explanation: finally runs after the try block whether an exception happens or not. For 2, the code prints T, then L, then F; for 0, it prints T, then E, then F, followed by !.

The key concept is that a finally clause always executes after the try statement finishes its current path. If no exception is raised, control goes from try to else to finally. If a matching exception is raised, control goes from try to except to finally.

  • With 2, 10 // 2 succeeds, so the trace is T, L, F.
  • With 0, 10 // 0 raises ZeroDivisionError, so the trace is T, E, F.
  • After both calls, the final print adds !.

That produces TLFTEF!. The closest mistake is skipping the second F, but finally still runs after the exception is handled.

  • The option missing the second F fails because finally still runs after ZeroDivisionError is caught.
  • The option starting with TEF fails because the loop processes 2 before 0.
  • The option with FF together fails because finally runs once per function call, immediately after else or except.

Question 28

Topic: Section 2: Exceptions

Review this code in a single module:

class AppError(Exception):
    pass

class ConfigError(AppError):
    pass

def parse_timeout(text):
    value = int(text)
    if value < 0:
        raise ConfigError("negative timeout")
    return value

try:
    print(parse_timeout("abc"))
except Exception as err:
    print("Settings error:", err)

The handler should catch only application-defined exceptions derived from AppError and let unrelated built-in exceptions such as ValueError propagate. What is the best fix?

Options:

  • A. Replace the handler with except ConfigError as err:

  • B. Add except AppError as err: before the existing except Exception as err:

  • C. Replace the handler with except AppError as err:

  • D. Replace the handler with except (AppError, Exception) as err:

Best answer: C

Explanation: Use the shared custom base class in the except clause. AppError catches ConfigError and any future app-defined subclasses, while built-in exceptions such as ValueError are no longer swallowed by a broad except Exception.

When several application-specific exceptions belong to the same family, the safest pattern is to derive them from a custom base class and catch that base class. Here, ConfigError inherits from AppError, so except AppError will handle it and any future subclasses in the same hierarchy.

Because ValueError comes from the built-in Exception branch rather than from AppError, it will bypass that handler and propagate normally. That is exactly what the requirement asks for. Leaving except Exception in place, or including Exception in a tuple, is still too broad because it matches unrelated errors raised inside the try block.

The key takeaway is to catch the narrowest custom base class that represents the exceptions you actually want to handle.

  • Catching only ConfigError is too narrow because the requirement includes future exceptions derived from AppError.
  • Adding a specific handler before the existing broad except Exception still leaves unrelated built-in exceptions caught by the generic block.
  • Using a tuple that includes Exception remains too broad, so built-in errors like ValueError are still caught accidentally.

Question 29

Topic: Section 3: Strings

A developer wants to copy each non-space character from a label and add - after it. What is printed by this Python 3 code?

label = "PC AP"
out = ""
for ch in label:
    if ch == " ":
        continue
    out += ch + "-"
print(out)

Options:

  • A. P-C-A-P-

  • B. P-C-A-P

  • C. PC-AP-

  • D. P-C- -A-P-

Best answer: A

Explanation: for ch in label iterates through the string one character at a time. The space is reached as its own character, but continue skips it, so only P, C, A, and P are added, each followed by -.

Strings are iterable, so for ch in label: reads characters from left to right, one at a time. For "PC AP", the loop sees P, C, a space, A, and P. When ch is a space, continue immediately starts the next iteration, so nothing is appended for that character. For every other character, out += ch + "-" adds the character and then a hyphen.

  • After P, out is P-
  • After C, out is P-C-
  • At the space, out stays P-C-
  • After A and P, out becomes P-C-A-P-

The key takeaway is that direct string iteration works character by character, including spaces unless your code skips them.

  • The option showing PC-AP- treats the string like two chunks, but direct iteration returns single characters.
  • The option keeping the space ignores that continue skips the append step when ch is ' '.
  • The option without the final hyphen misses that ch + "-" runs for every non-space character, including the last P.

Question 30

Topic: Section 2: Exceptions

A developer is debugging a calculation and wants one except block to print the caught exception’s class name and message. What is printed?

def run(value):
    try:
        print(10 / int(value))
    except (ValueError, ZeroDivisionError) as err:
        print(type(err).__name__, err.args[0])
    finally:
        print("done")

run("0")

Options:

  • A. ZeroDivisionError division by zero

  • B. ValueError invalid literal for int() with base 10: ‘0’ done

  • C. ZeroDivisionError (‘division by zero’,) done

  • D. ZeroDivisionError division by zero done

Best answer: D

Explanation: The grouped handler catches the ZeroDivisionError raised by 10 / 0. Because as err binds the exception object to a local name, the code can read both type(err).__name__ and err.args[0], and finally still prints done.

except ... as name assigns the caught exception instance to a local name inside that handler. In this code, int("0") succeeds and returns 0, but 10 / 0 raises ZeroDivisionError. That exception matches the grouped handler and is bound to err.

  • type(err).__name__ returns ZeroDivisionError
  • err.args is a tuple, so err.args[0] is division by zero
  • finally runs whether or not an exception was raised

So the program prints the exception type and message, then prints done. The closest wrong choice confuses the whole args tuple with its first element.

  • Wrong exception the option showing ValueError fails because int("0") does not raise an exception.
  • Tuple confusion the option showing ('division by zero',) uses err.args, but the code prints err.args[0].
  • Missing cleanup the one-line option ignores that finally always runs after the handled exception.

Question 31

Topic: Section 4: Object-Oriented Programming

A developer expects this program to print ok, but it prints bad output instead.

class Ticket:
    def __init__(self, code):
        self.code = code

    def _str_(self):
        return self.code

t = Ticket("R-5")

try:
    assert str(t) == "R-5"
except AssertionError as err:
    print("bad output")
else:
    print("ok")
finally:
    print("done")

Which change fixes the problem so the else block runs?

Options:

  • A. Move print("ok") into finally

  • B. Make _str_ return str(self.code)

  • C. Catch Exception instead of AssertionError

  • D. Rename _str_ to __str__

Best answer: D

Explanation: The method is misspelled. str(t) looks specifically for __str__, so Python ignores _str_, the assertion fails, and the except block runs instead of else.

str(obj) uses a special method named exactly __str__(). In this class, the method is written as _str_, which is just a normal method and is not used by str(t). Because of that, str(t) does not return "R-5", so the assert statement raises AssertionError.

The exception flow is then:

  • try: evaluates the failed assertion
  • except AssertionError as err: catches the failure and prints bad output
  • else: skipped because an exception occurred
  • finally: always runs and prints done

Renaming _str_ to __str__ fixes the object’s string conversion behavior; changing handlers or finally does not fix the underlying class problem.

  • Making _str_ return str(self.code) still leaves the special method misspelled, so str(t) never calls it.
  • Catching Exception changes the handler, but the assertion still fails and the else block still does not run.
  • Moving print("ok") into finally changes output placement, not the broken string-conversion behavior.

Question 32

Topic: Section 4: Object-Oriented Programming

A developer wants one Wallet object to preserve its balance across several method calls. The code should print 12, but it fails:

class Wallet:
    def __init__(self, amount=0):
        self.amount = amount

    def add(self, value):
        amount += value

    def spend(self, value):
        self.amount -= value

w = Wallet(10)
w.add(5)
w.spend(3)
print(w.amount)

Which change is the best fix?

Options:

  • A. Replace amount += value with Wallet.amount += value.

  • B. Replace amount += value with self.amount += value.

  • C. Change def add(self, value): to def add(value, self):.

  • D. Replace amount += value with amount = self.amount + value.

Best answer: B

Explanation: Object state for one instance is stored in its instance attributes, accessed through self. Changing add to use self.amount += value lets the same Wallet object move from 10 to 15 to 12 across the two method calls.

In Python, an object’s changing state is usually kept in instance attributes such as self.amount. Inside add, the bare name amount is not the wallet’s stored balance; it is treated as a local variable name, so amount += value does not update the instance.

After replacing that line with self.amount += value, the state changes happen on the same object:

  • Wallet(10) sets w.amount to 10
  • w.add(5) changes w.amount to 15
  • w.spend(3) changes w.amount to 12

The key point is to update the instance attribute, not a local name or a class attribute.

  • Class vs instance Using Wallet.amount targets a class attribute, not this object’s stored balance.
  • Local assignment Writing amount = self.amount + value only creates a local variable unless the result is assigned back to self.amount.
  • Wrong signature Swapping the parameter order breaks normal instance method binding, because Python passes the instance as the first argument.

Question 33

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A developer is inspecting a package initializer. The text file pkg/__init__.py contains exactly these three lines, and each line ends with \n:

from .tools import run
__all__ = ["run"]
_hidden = 0

What value is assigned to rest after this code runs?

with open("pkg/__init__.py", "r") as f:
    f.readline()
    rest = f.readlines()

Options:

  • A. ['__all__ = ["run"]\n', '_hidden = 0\n']

  • B. ['__all__ = ["run"]', '_hidden = 0']

  • C. ['from .tools import run\n', '__all__ = ["run"]\n', '_hidden = 0\n']

  • D. '__all__ = ["run"]\n_hidden = 0\n'

Best answer: A

Explanation: readlines() reads from the current file position to the end of the file and returns a list of the remaining lines. Because readline() already consumed the first line, only the last two lines appear in rest, and their \n characters remain.

The key concept is that file reading methods move the file cursor. After f.readline(), the first line has already been read, so the cursor is positioned at the start of the second line. Calling f.readlines() then reads all remaining lines up to end-of-file and returns them as a list of strings.

Because the file is opened in text mode and the stem states that each line ends with \n, each returned list element keeps its trailing newline. That means rest contains the second and third lines only: __all__ = ["run"]\n and _hidden = 0\n.

The closest mistake is assuming readlines() starts again from the top of the file; it does not.

  • The option with all three lines ignores that the first line was already consumed by readline().
  • The single string option confuses readlines() with read(), which returns one string.
  • The list without \n assumes line endings are stripped automatically, but readlines() preserves them here.

Question 34

Topic: Section 4: Object-Oriented Programming

A developer is testing a class method that updates object state and may raise an exception. What is the output of this program?

class LimitError(Exception):
    pass

class Counter:
    def __init__(self):
        self.value = 1

    def bump(self, step):
        self.value += step
        if self.value > 3:
            raise LimitError(self.value)
        return self.value

c = Counter()

try:
    print(c.bump(1))
    print(c.bump(2))
except LimitError as e:
    c.value -= 1
    print(e.args[0], c.value)
finally:
    print(c.value)

Options:

  • A. 2; 4 4; 4

  • B. 2; 3 3; 3

  • C. 2; 4; 3

  • D. 2; 4 3; 3

Best answer: D

Explanation: The second call to bump() changes the object’s state before the exception is raised. That means the exception carries 4, the except block changes the stored value to 3, and finally prints 3.

bump() modifies the instance attribute through self, and that change is not undone when the exception is raised. On the first call, value goes from 1 to 2, so 2 is printed. On the second call, value goes from 2 to 4, then LimitError(4) is raised before the outer print() can print a return value.

  • e.args[0] is 4 because that was passed to the exception.
  • The handler executes c.value -= 1, so the object’s state becomes 3.
  • The finally block always runs and prints the current value, which is 3.

The key point is that the method updates object state first, and the exception handler updates it again afterward.

  • The option showing 3 3 treats the exception argument as if it changed after the handler modified the object.
  • The option showing 4 4 ignores the c.value -= 1 statement inside the except block.
  • The option showing only 4 before the last line misses that print(e.args[0], c.value) outputs two values on one line.

Question 35

Topic: Section 2: Exceptions

A developer expects invalid age data to be handled, but this program stops with an uncaught exception when run as shown.

def load_age(record):
    try:
        age = int(record["age"])
        print(100 / age)
    except KeyError:
        print("missing age")
    except ZeroDivisionError:
        print("age cannot be zero")

load_age({"age": "abc"})

What is the best explanation?

Options:

  • A. ZeroDivisionError occurs first, so the handler order is wrong.

  • B. The try block protects only the division line.

  • C. int("abc") raises ValueError, and no handler catches it.

  • D. record["age"] raises KeyError because the value is invalid.

Best answer: C

Explanation: The try block covers both statements, but only KeyError and ZeroDivisionError are handled. Because int(record["age"]) raises ValueError for 'abc', that exception does not match any except clause and propagates out of the function.

An exception is handled only when its type matches one of the except clauses attached to the current try block. In this code, the try block includes both the conversion and the division. With {"age": "abc"}, the dictionary lookup succeeds because the key exists, but int("abc") raises ValueError.

  • KeyError would be caught only if "age" were missing.
  • ZeroDivisionError would be caught only if conversion succeeded and the value became 0.

Since no except ValueError clause exists, the exception is not handled here and continues upward. The closest wrong idea is confusing an invalid value with a missing key.

  • Missing key confusion fails because the "age" key exists; the problem is converting its value.
  • Wrong operation order fails because division cannot run until the integer conversion succeeds.
  • Try scope misunderstanding fails because every indented statement inside the try suite is protected.

Question 36

Topic: Section 1: Modules and Packages

A developer has two package roots on the same machine:

/proj/lib/tools/__init__.py
/proj/lib/tools/helper.py      # version = "new"

/legacy/tools/__init__.py
/legacy/tools/helper.py        # version = "old"

This script should import the newer package, but it prints old:

import sys

sys.path = ["/legacy", "/proj/lib"] + sys.path
from tools import helper
print(helper.version)

Which change best fixes the import behavior while keeping both package roots available?

Options:

  • A. Replace from tools import helper with import helper.

  • B. Delete the package __pycache__ directories.

  • C. Put "/proj/lib" before "/legacy" in sys.path.

  • D. Add "/proj/lib/tools" to sys.path.

Best answer: C

Explanation: Python resolves imports by scanning sys.path in order for the top-level package name. Because "/legacy" is listed first, its tools package is imported; placing "/proj/lib" first makes the intended package load instead.

For from tools import helper, Python first looks for a top-level package named tools by checking each directory in sys.path from left to right. Here, both "/legacy" and "/proj/lib" contain a valid tools package, so the first match is used. Since "/legacy" comes first, Python imports legacy/tools/helper.py and prints old.

A sys.path entry must be the parent directory of the package, not the package directory itself. Reordering the two roots fixes the issue while keeping both available. __pycache__ only stores bytecode caches, and changing the import to import helper would ask for a different top-level module.

  • Adding "/proj/lib/tools" uses the package directory instead of its parent, so from tools import helper still would not locate tools correctly.
  • Deleting __pycache__ may force recompilation, but it does not change which matching package is found first.
  • Replacing the import with import helper looks for a top-level module named helper, not the helper submodule inside tools.

Question 37

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

In Python 3, what is the exact output of this code?

def report(func, values):
    print(func.__name__, [func(v) for v in values])

def cube(x):
    return x ** 3

nums = [1, 2]
report(cube, nums)
report(lambda x: x + 1, nums)

Options:

  • A. cube [1, 8] cube [2, 3]

  • B. cube [1, 8] [2, 3]

  • C. cube [1, 8] lambda [2, 3]

  • D. [1, 8] [2, 3]

Best answer: B

Explanation: cube is created with def, so its function name is cube. The second call passes a lambda expression directly, and lambda-created function objects report their name as <lambda>. Applying them to [1, 2] produces [1, 8] and [2, 3].

A function defined with def gets the name you assign in the definition, so func.__name__ is cube in the first call. The list comprehension then applies cube to each number, giving [1, 8].

In the second call, report receives an inline lambda x: x + 1. A lambda creates an anonymous function object, so its displayed name is <lambda>. Applying that function to 1 and 2 gives [2, 3].

This reflects the usual distinction: def is preferred for named, reusable functions, while lambda is commonly used for short one-off callables passed directly where needed.

  • The option showing lambda without angle brackets fails because Python reports a lambda function name as <lambda>.
  • The option showing <lambda> on both lines fails because the first call uses the named function cube.
  • The option showing cube on both lines fails because the second call passes a different inline function, not cube.

Question 38

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

A developer writes a helper function that accepts a lambda and calls it for each value:

def collect(values, test):
    result = []
    for v in values:
        if test(v):
            result.append(v * 10)
    return result

print(collect([1, 2, 3, 4], lambda x: x % 2 == 0))

What is printed?

Options:

  • A. [10, 20, 30, 40]

  • B. [False, True, False, True]

  • C. [2, 4]

  • D. [20, 40]

Best answer: D

Explanation: A lambda can be passed to a user-defined function just like any other callable. Here, collect() uses the lambda as a test, so only even numbers pass, and the function stores v * 10 for those values.

The core idea is that a lambda is just a function object, so it can be passed as an argument and called inside another function. In this snippet, collect() receives the lambda as test and evaluates test(v) for each item in [1, 2, 3, 4].

  • For 1, the lambda returns False
  • For 2, it returns True, so 20 is appended
  • For 3, it returns False
  • For 4, it returns True, so 40 is appended

The lambda controls which values are accepted, but the appended values come from v * 10. That is why the output is a list of scaled even numbers, not the original even numbers or the boolean test results.

  • The option showing only [2, 4] misses that the function appends v * 10, not v.
  • The option showing all four multiplied values ignores the if test(v) condition.
  • The option showing booleans confuses the lambda’s return values with the values added to result.

Question 39

Topic: Section 5: Miscellaneous (List Comprehensions, Lambdas, Closures, I/O)

Assume open() succeeds. In this binary I/O example, a developer mixes a text string with a byte-oriented buffer.

class BinaryDataError(Exception):
    pass

buf = bytearray(b"AB")

try:
    try:
        with open("out.bin", "wb") as f:
            f.write(buf)
            f.write("C")
    except TypeError as ex:
        raise BinaryDataError("encode text before binary write")
    else:
        print("saved")
except BinaryDataError as ex:
    print(ex.args[0])
finally:
    print(buf.decode())

What is printed?

Options:

  • A. saved then AB

  • B. TypeError then AB

  • C. encode text before binary write then ABC

  • D. encode text before binary write then AB

Best answer: D

Explanation: Binary file writes accept bytes-like objects such as bytearray, not str. The first write succeeds, the second raises TypeError, that error is replaced by a custom exception, and finally still runs.

Binary I/O in Python works with bytes-like data, not text strings. Here, buf is a bytearray, so f.write(buf) is valid in 'wb' mode. But f.write("C") sends a str to a binary stream, which raises TypeError. The inner except catches that and raises BinaryDataError("encode text before binary write"), so the inner else block is skipped. The outer except prints the custom exception message, and the finally block runs regardless of success or failure. Since the file write does not change buf, buf.decode() still prints AB.

The closest wrong idea is the output with saved, but else runs only when no exception occurs in the inner try.

  • The option with saved fails because the second write raises an exception, so the inner else block does not run.
  • The option with TypeError fails because that exception is caught and replaced with BinaryDataError.
  • The option ending with ABC fails because writing to the file does not modify the original bytearray.

Question 40

Topic: Section 4: Object-Oriented Programming

What is printed by this code?

class LimitError(Exception):
    pass

class Tracker:
    def __init__(self):
        self.value = 1
        self.log = []

    def apply(self, x):
        try:
            self.value += x
            if self.value > 4:
                raise LimitError(self.value)
        except LimitError as e:
            self.value -= 2
            self.log.append(f'fix:{e.args[0]}')
        else:
            self.log.append('ok')
        finally:
            self.value += 1

c = Tracker()
for n in (2, 3, -1):
    c.apply(n)
print(c.value, ','.join(c.log))

Options:

  • A. 3 ok,fix:7,fix:5

  • B. 4 ok,fix:7,fix:5

  • C. 4 ok,ok,fix:5

  • D. 4 ok,fix:7,ok

Best answer: B

Explanation: The same Tracker object is used for all three calls, so its value carries forward each time. When value becomes greater than 4, the except block adjusts it and records the exception argument, and finally still adds 1.

This item is about tracing instance state across repeated method calls. c.value and c.log belong to the same object, so each apply() call starts from the previous call’s final state.

  • After apply(2): value goes from 1 to 3, no exception occurs, else adds ok, then finally makes value 4.
  • After apply(3): value goes from 4 to 7, LimitError(7) is raised, except changes value to 5 and logs fix:7, then finally makes it 6.
  • After apply(-1): value goes from 6 to 5, another LimitError(5) is raised, except changes value to 3 and logs fix:5, then finally makes it 4.

So the output is 4 ok,fix:7,fix:5. A common mistake is forgetting that finally runs even when an exception is handled.

  • The option with final value 3 overlooks that finally runs after the third handled exception and increments value again.
  • The option ending with ok on the third call ignores that the third call starts from value == 6, so adding -1 still triggers the exception.
  • The option showing ok on the second call misses that else runs only when no exception is raised.

Continue with full practice

Use the PCAP-31-03 Practice Test page for the full IT Mastery route, mixed-topic practice, timed mock exams, explanations, and web/mobile app access.

Try PCAP-31-03 on Web View PCAP-31-03 Practice Test

Focused topic pages

Free review resource

Read the PCAP-31-03 Cheat Sheet on Tech Exam Lexicon for concept review before another timed run.

Revised on Thursday, May 14, 2026