Python Modules, Debugging and Testing Basics

Learn to build on code that already exists and to make your own code work reliably. Import modules from Python's standard library such as math and random, write your own modules and install packages from PyPI with pip. Then read tracebacks and error messages, find bugs with a step-by-step debugging process, and check your functions with assert statements and first unit tests.

BeginnerLesson 9 of 10 in Python for Beginners: Introduction to Programming

What you will learn

  1. Bring ready-made code into a program with import statements, use standard-library modules such as math and random, split your own code into modules and install third-party packages with pip.
    • Bring code into a program with import statements
      Use import, from … import and as correctly, reach names with dot notation and tell the import errors apart.
    • Calculate and simulate with math, random and other standard modules
      Use the main functions of math and random, avoid their common traps and know a few other everyday standard-library modules.
    • Write and import your own modules
      Split code into your own .py files, import them, avoid name clashes and use if __name__ == "__main__": so a file can be both run and imported.
    • Install and manage packages with pip
      Install, inspect, upgrade and remove third-party packages from PyPI, import them under the right name and solve the usual installation problems.
  2. Use Python's error output to find where a program failed and why, and tell syntax errors, runtime errors and logic errors apart.
    • Read a traceback from the bottom up
      Find the exception type, the message, the failing line and the chain of calls that led there.
    • Recognize common errors and their causes
      Link the most frequent exception types to their usual causes and fixes, and spot logic errors that produce no message.
  3. Locate the cause of a bug with a step-by-step process instead of guessing, and inspect a running program with print statements and a debugger.
    • Follow a step-by-step debugging process
      Reproduce a bug, form and test a hypothesis, narrow down the cause, fix it and retest.
    • Inspect a program with print statements, trace tables and a debugger
      Watch how values change while code runs, by adding prints, tracing by hand or pausing at breakpoints.
  4. Use assert to state what must be true, write small automated tests for your functions and run and read them with the standard unittest module.
    • Check assumptions with assert statements
      Write assert statements with clear messages, and know when assertions are the right tool and when they are not.
    • Write first unit tests for your functions
      Write test functions with assertions, choose typical, boundary and edge cases, and use failing tests to find and fix bugs.
    • Organize and run tests with unittest
      Write unittest.TestCase tests, run them and read the report of passes, failures and errors.

Python Modules and Debugging study guide

4 chapters

1

Reuse modules and install packages

Imported code lets a small program use calculations, simulations and tools written elsewhere. You will choose an import form, work with standard-library functions, and arrange your own functions so another file can reuse them. Installation adds a separate question: which Python receives a third-party package? Understanding that link makes it easier to diagnose a missing module and keep each program's names clear.

1.1 Import a module or selected names

A module holds Python code that another program can reuse. Most modules are .py files. The standard library comes with Python; third-party code needs a separate installation. A package groups related modules, commonly in a folder.

Match the call to the import

An import creates names in your program. The form you choose determines how you call the imported code.

StatementName createdExample call
import mathmathmath.sqrt(16)
from math import sqrtsqrtsqrt(16)
import statistics as statsstatsstats.mean([2, 4, 9])
from random import randint as rollrollroll(1, 6)

Dot notation means looking up a name inside a module or object. In math.sqrt(16), Python finds sqrt inside math and calls it with 16. The result is 4.0. The constant math.pi needs no call parentheses because it is a value, not a function.

After import math, sqrt(16) alone raises NameError: the program has a name for the module, but no separate name for sqrt. After only from math import sqrt, the reverse mistake, math.sqrt(16), also raises NameError because math was not bound. An alias replaces the imported binding's usual name: import statistics as stats creates stats, not both names.

Work the same calculation two ways

A right triangle has sides 3 and 4. First square the sides and add them; then take the square root.

import math

length = math.sqrt(3**2 + 4**2)
print(length)  # 5.0

The expression inside the call becomes 9 + 16, then 25; sqrt returns 5.0. With from math import sqrt, write length = sqrt(3**2 + 4**2) instead. Using the module prefix makes a function's source visible and keeps its other names out of your program's name space. The binding math itself can still replace an earlier variable called math.

Keep names clear

Avoid from math import *. This wildcard import brings public names into your program without naming them individually. For example, e = 5 followed by that import changes e to 2.718281828459045. An unrelated-looking import can therefore change a later calculation. PEP 8 recommends explicit imports and puts imports at the top, with one module per line: standard library first, third-party packages next, your own modules last.

Python runs a module's top-level code on its first import during a program run. Later imports reuse the loaded module; they do not start it again.

Use the error to choose the fix
ErrorWhat to inspect
ModuleNotFoundError from import mathhThe module's spelling and installation
ImportError from from math import square_rootThe name requested from the module
AttributeError from math.sqroot(16)The name after the dot

In the interactive shell, run import math, then dir(math) to list its names. Run help(math.sqrt) to read a function's documentation. The official Python library reference explains its arguments and result.

Key Takeaways

  • import module creates a module binding; reach its contents with dot notation.
  • from module import name creates the selected binding; as chooses an alias.
  • Explicit imports keep the origin of names clear and reduce accidental replacements.
  • Import errors distinguish an unfound module, an unavailable imported name and a missing attribute.
  • dir() lists names; help() explains how to use one.

1.2 Choose math and random functions

The standard library supplies tested tools for calculations and simulations. Choosing a function starts with the result you need: an integer count, a rounded measurement, one random item or a reordered list.

Rounding has several meanings

After import math, math.sqrt(16) returns the float 4.0. math.pi and math.e supply the constants 3.141592653589793 and 2.718281828459045. math.factorial(5) multiplies the whole numbers from 1 through 5 and returns 120.

ExpressionResultMeaning
math.floor(7.9)7Greatest integer no larger than the value
math.ceil(2.1)3Smallest integer no smaller than the value
math.floor(-2.5)-3Down goes toward negative infinity
int(-3.5)-3Conversion cuts toward zero
round(2.5)2An exact halfway value rounds to even
round(3.5)4The neighboring even integer is 4

For 23 square meters of wall and 10 square meters per paint can, division gives 2.3 cans. Buying whole cans requires math.ceil(23 / 10), which returns 3. round(2.3) gives 2 and leaves too little paint.

The built-in round() needs no import. For a circle of radius 2, round(math.pi * 2 ** 2, 2) gives 12.57. Rounding changes a number's precision; it is a different job from buying enough whole containers.

Check domains and float comparisons

math.sqrt(-1) raises ValueError because there is no real square root for that input. Calculated floats also need care: binary storage means 0.1 + 0.2 == 0.3 is False. math.isclose(0.1 + 0.2, 0.3) is True; use it for an approximate comparison of calculated floats.

Select the random operation

The random module uses an algorithm to make pseudo-random results.

CallResult
random.randint(1, 6)An integer from 1 through 6, both included
random.randrange(1, 6)An integer from 1 through 5
random.random()A float at least 0.0 and less than 1.0
random.uniform(a, b)A float between the supplied endpoints
random.choice(items)One item
random.sample(items, k)Items from k positions, without replacement
random.shuffle(items)Reorders the existing list; returns None

Keep random.shuffle(cards) on its own line. Assigning its result back to cards loses the list binding by replacing it with None. Sampling positions is also different from requiring distinct values: random.sample([1, 1], 2) returns [1, 1].

Run a repeatable simulation
import random

sevens = 0
for _ in range(1000):
    if random.randint(1, 6) + random.randint(1, 6) == 7:
        sevens += 1
print(sevens)

Six of 36 equally likely pairs total 7, so the count is usually near 167, but it changes between runs. To reproduce a sequence for debugging, call random.seed(42) before generating the values, using the same Python version and call sequence. For passwords or security tokens, use secrets.

Find other everyday tools

statistics.mean([2, 4, 9]) returns 5; statistics.median([2, 4, 9]) returns 4. datetime.date.today() gives today's date. After import sys, sys.argv holds the script name followed by its arguments: running python greet.py Ada supplies ['greet.py', 'Ada'].

Key Takeaways

  • floor, ceil, int and round answer different rounding questions.
  • Use math.isclose when comparing calculated floats.
  • randint includes both endpoints; randrange excludes its stop.
  • sample selects positions without replacement, so duplicate input values can repeat.
  • shuffle changes a list in place and returns None.
  • A fixed seed helps reproduce random behavior; security tokens need secrets.

1.3 Make a file reusable without starting its program

A function becomes easier to reuse when it lives in a focused module. Another program can import that function, and a test can check it without asking a person to run the whole application.

Split the function from its caller

Save this code as geometry.py:

"""Calculations for simple shapes."""
import math

def circle_area(radius):
    """Return the area of a circle with the given radius."""
    return math.pi * radius ** 2

In the same folder, save this caller as main.py:

import geometry

print(geometry.circle_area(2))

Run python main.py. Python finds geometry, creates its function, and then the caller runs that function. The printed result is 12.566370614359172. Import without the .py ending. A valid module name uses letters, digits and underscores with a valid first character; my_tools.py works with import my_tools, while import my-tools is invalid syntax.

The strings at the starts of the module and function are docstrings. They describe the code's purpose and appear in help(geometry) and help(geometry.circle_area).

Understand what importing runs

On the first import, Python executes the module's top-level statements in order. A def statement creates a function; it does not execute that function's body. An unguarded print(circle_area(2)) at the bottom of geometry.py, however, would print during import.

That side effect matters when a module contains input prompts or a program loop. A test that only wants circle_area should not have to start the program and answer its prompts.

Guard the launch, keep the function available

Add this below the reusable function in geometry.py:

def main():
    print(circle_area(2))

if __name__ == "__main__":
    main()

Python sets __name__ for each module. A direct run, python geometry.py, sets it to "__main__", making the condition true and calling main(). Importing geometry sets it to "geometry", so Python skips the call. The definitions above the guard remain available in both cases.

Do not place circle_area itself inside the guard. An importer needs that definition even when the launch condition is false. The guard controls when the application starts; it does not prevent Python from loading the module.

Know where Python searches

Python checks built-in modules such as sys, then the folders in sys.path in order. Normally the running script's folder comes first in that path, followed by standard-library and installed-package folders. Keeping your own files beside the running script is a simple arrangement for a beginner project. A module outside the search path cannot be found by an ordinary import.

This order can also hide a library module. If you save your program as random.py, its import random can load your file instead of the library. A later random.randint(1, 6) then raises AttributeError. Rename the file, for example to dice.py; Python 3.13 and later may include a renaming hint in the error.

A loaded module is reused within the same run. In an interactive shell, editing the file and importing it again keeps the loaded version. Restart the shell before testing your changes.

Key Takeaways

  • Import your own .py file using its module name without the extension.
  • An import executes top-level statements, while function bodies wait for calls.
  • Keep reusable definitions outside the __main__ guard and the program launch inside it.
  • A local file can hide a library module with the same name.
  • Restart an interactive shell to test an edited module afresh.

1.4 Install packages for the Python you run

Third-party packages add code that does not come with Python. PyPI, the Python Package Index at https://pypi.org, hosts these projects. pip downloads and installs them for a particular Python installation.

Use the terminal, then use Python

A terminal command and a Python statement belong in different places. The >>> prompt means you are in Python's interactive shell. Entering pip install requests there gives SyntaxError; Python cannot read it as a statement.

In your terminal, run:

python -m pip install cowsay

Then put this in a Python program:

import cowsay

cowsay.cow("Hello, Ada")

The program prints a text cow beside a speech bubble containing the message. Installation makes the package available; the import creates the program's binding for it.

Match pip to your interpreter

The -m pip form asks the named Python to run its own pip. Use the command that starts your programs: often python3 on macOS or Linux, or py on Windows.

python3 -m pip install cowsay
python3 game.py

Both lines select the same Python command. If an installation reports success but a script still raises ModuleNotFoundError, compare the Python used to install with the one used to run the script. An editor may have selected a different interpreter from your terminal. Use the selected interpreter's pip before changing the import.

Inspect and maintain an installation
Terminal commandJob
python -m pip listList installed packages and versions
python -m pip show requestsShow details, including its installation location
python -m pip install --upgrade requestsUpdate to the newest available version
python -m pip install "requests==2.32.3"Select that exact version
python -m pip uninstall requestsRemove the package

An exact version is useful when you need that particular release's behavior. show helps check which location pip actually changed.

Check the name in the documentation

The distribution name typed for pip can differ from the import name. Install beautifulsoup4, then use import bs4; install Pillow, then use import PIL. Repeatedly installing an import name is not a useful fix when the documentation names a different distribution.

Python's standard library, including math, random and unittest, already comes with Python. These modules do not require a pip installation.

Respect a managed installation

Some operating systems and package managers protect their Python with an externally-managed-environment error. This means a direct install is blocked to keep the managed installation consistent. Use a virtual environment, a separate Python and package folder for the project. Do not force the installation with sudo pip install.

Anyone can publish on PyPI. Before installing, check the exact spelling and read the project's page and documentation. Lookalike names can be typosquatting attempts. Prefer established, maintained packages, and install the ones your program actually needs.

Key Takeaways

  • Run pip commands in a terminal and imports in Python code.
  • Use python -m pip with the same interpreter command that runs the program.
  • Check the documentation for the import name; it may differ from the install name.
  • Use pip to inspect, update, select a version or remove a package.
  • A managed Python may require a project virtual environment.
  • Check package spelling and documentation before installing third-party code.
2

Read errors and tracebacks

Python's error output records both the operation that failed and the calls that reached it. Reading those clues in order helps you find a useful starting point instead of changing the first line you recognize. You will follow a traceback back to its input, distinguish common exception types, and recognize the quieter case in which a program runs normally but produces the wrong result. The expected behavior gives you something concrete to check.

2.1 Follow a traceback back to the bad input

A traceback tells you which calls were active when an unhandled exception stopped the program. It gives both a place to begin reading and a path back toward the value that caused the failure.

Start with the final line

Consider this complete program in scores.py:

def average(values):
    return sum(values) / len(values)

def report(scores):
    avg = average(scores)
    print(f"Average: {avg:.1f}")

report([])

The final line of its error report is ZeroDivisionError: division by zero. This names the exception type and describes the failed operation. Start there, then read upward to the code location immediately above it.

A simplified traceback, with expression markers omitted, is:

Traceback (most recent call last):
  File "scores.py", line 8, in <module>
    report([])
  File "scores.py", line 5, in report
    avg = average(scores)
  File "scores.py", line 2, in average
    return sum(values) / len(values)
ZeroDivisionError: division by zero
Read each frame as an active call

A frame gives the file, line number and function, followed by the relevant code. in <module> means top-level code. The oldest call is at the top; the most recent is at the bottom. Reading from the bottom reverses that order so you can ask what failed before asking how execution got there.

  1. Line 2 divides by len(values). For that divisor to be zero, values must be empty.
  2. Line 5 calls average(scores), so inspect the scores passed by report.
  3. Line 8 supplies [], the empty list. That explains how the zero divisor reached the calculation.

The failing line and the cause are related, but they can be in different places. Replacing the division without deciding what an empty list should mean would hide the design question. Either the caller must avoid an empty list or the function needs a defined empty-input behavior.

Use highlights without depending on their exact shape

Python 3.11 and later can add ^ and ~ markers to show the failing part of an expression. They help when one line contains several operations. The exact highlighting varies between versions, but the call locations and exception message still give the reading order.

A traceback can contain frames from the standard library or an installed package. Find the lowest frame in your own code and inspect the arguments you supplied there. A library frame is a location clue; it is not evidence that the library contains the bug.

Recognize a syntax report

A syntax error in the script being started prevents that script from running. Python reports a file, line, marker and message without the usual traceback header. For example, if True is missing its colon; total = sum([1, 2, 3] leaves a parenthesis open.

The marked place is where Python noticed it could not parse the code. When that line looks valid, check the preceding line for an unclosed quote or parenthesis. There is no chain of runtime calls to follow when the starting script has not run.

An ordinary run stops at its first unhandled exception. Fix that problem and run again to expose anything later in the program.

Key Takeaways

  • Read the exception type and message first, then the failing frame and its callers.
  • Frames run from the oldest active call at the top to the newest at the bottom.
  • Trace the bad value backward; its source can precede the crash.
  • A syntax error in the starting script is reported before that script runs.

2.2 Tell a wrong name, type, value and result apart

An error's name narrows the search, and its message adds details about the values involved. A program can also be wrong without raising an exception, so checking its expected output remains part of finding errors.

Separate three kinds of problem
KindWhat happensExample
Syntax errorPython cannot parse the codeA missing colon after if
Runtime errorExecution reaches an invalid operationDividing by zero
Logic errorExecution finishes with the wrong behaviorA loop skips a price

IndentationError is a kind of SyntaxError. A function body needs indentation; a missing indented block stops the code before it runs. Also inspect unclosed quotes or parentheses and misspelled keywords.

Read names and types closely

A NameError means the program does not know a name at that point. In prnt("hi"), the intended built-in is print. A name can also be used before assignment, or an import can be missing.

A TypeError means an operation received an unsuitable type or a call received the wrong number of arguments. "Age: " + 30 mixes a string and an integer. Write "Age: " + str(30) or f"Age: {30}". If area needs w and h, area(3) is also a TypeError, this time for a missing argument.

The message distinguishes these causes. In can only concatenate str (not "int") to str, Python names the types that met. After age = input("Age: "), age is text. age + 1 fails; for a whole-number entry, int(age) + 1 performs arithmetic.

A ValueError means the input's type is accepted, but its value is unsuitable. int("12.5") receives a string, which is an accepted input type, but the text does not represent a whole number in the required form. math.sqrt(-1) is another unsuitable value for a real-number calculation.

Check positions, keys and attributes
ExceptionExampleCause
IndexError[4, 6, 10][3]The last valid index is 2
KeyError{"apples": 3}["pears"]The dictionary lacks "pears"
ZeroDivisionError10 / 0The divisor is zero
AttributeErrormath.sqroot(16)The module lacks sqroot
ModuleNotFoundErrorimport mathhNo module is found under that name
ImportErrorfrom math import square_rootThe module lacks that imported name

Zero divisors also fail with // and %. A missing list position is a different lookup from a missing dictionary key, even though both involve asking for something absent.

Detect a result that looks plausible
def total_price(prices):
    total = 0
    for i in range(1, len(prices)):
        total += prices[i]
    return total

For [4, 6, 10], this returns 16 instead of 20. There is no exception: indexes 1 and 2 exist, so every operation is legal. The loop starts too late and misses index 0. The error becomes visible when you compare the output with the intended total.

For an unfamiliar exception, look up its type and the general message in the official Python documentation. Remove your own paths and input values from a search; they rarely explain the general rule.

Key Takeaways

  • Syntax errors prevent parsing; runtime errors occur during execution; logic errors give wrong behavior.
  • NameError concerns a binding; TypeError concerns a type or call arguments.
  • ValueError concerns an unsuitable value supplied in an accepted type.
  • IndexError, KeyError and AttributeError refer to different kinds of lookup.
  • Checking output against an expected result can reveal a logic error with no traceback.
3

Find the cause of a bug

A wrong result becomes easier to investigate when you can reproduce it and say exactly how it differs from the expected result. You will turn that observation into a testable guess, narrow the search to a small piece of code, and inspect the values that matter. Prints, trace tables and debugger controls offer different ways to watch execution. After a focused fix, checking earlier working cases helps reveal any behavior the change affected.

3.1 Test a cause before changing the code

A bug is a difference between intended behavior and actual behavior. Debugging finds the cause and repairs it. A useful debugging session turns an observation into a testable explanation, then changes the code because of evidence.

Make the failure repeat

Record a specific input and the result it produces. For the price function that starts its loop at index 1, use [4, 6, 10]. The expected total is 20; the actual total is 16. These concrete values let you check whether each investigation still concerns the same bug.

The difference is 4, and the actual result equals 6 + 10. A specific hypothesis is that the first price is skipped. That hypothesis predicts something you can inspect: the loop will never visit index 0.

Follow six steps
  1. Reproduce: find an input that shows the bug reliably.
  2. State expected and actual results: calculate the expected result independently.
  3. Form a hypothesis: name a possible cause and its predicted behavior.
  4. Test it: inspect values with prints, a trace table or a debugger.
  5. Fix the cause: make one focused change.
  6. Retest: run the failing input and earlier working cases.

Print the index inside the loop. For this input, the only indexes are 1 and 2, confirming the prediction. Starting range at 0 fixes the skipped item. Iterating directly over the prices removes the need to manage an index:

def total_price(prices):
    total = 0
    for price in prices:
        total += price
    return total

The repaired function returns 20 for [4, 6, 10], 5 for [5] and 0 for []. The one-item case is useful because the buggy loop previously returned 0 for it.

Adding 4 to every answer would repair the observed total but leave the cause. For [5], that patch would return 4; the missing amount depends on the input. Check whether a proposed fix explains all the evidence before keeping it.

Narrow a larger search

Divide and conquer checks the values that feed the wrong result at a point partway through execution. If a needed value is already wrong, inspect the earlier code that produced it. If the needed values are still right, follow their later processing. Repeat inside the remaining section.

You can also call one function on its own or temporarily remove unrelated code. A minimal reproducible example keeps the shortest code and smallest input that still fail. For a problem first seen in hundreds of records, finding one or two records that trigger it makes every inspection easier.

Check what the data really is

A displayed 5 may be the string "5". "5" + "3" produces "53", while numeric addition gives 8. Inspect type(value) to check the type. Use repr(value) to reveal spaces and newline characters that an ordinary print can hide, such as ' Ada'.

Explain the code aloud, line by line, including what each loop actually visits and what each function returns. This is rubber duck debugging: explaining a step often exposes the gap between the intended operation and the written one.

Change one cause at a time so you can connect the result to the edit. Retest the cases that used to work as well as the one that failed.

Key Takeaways

  • Reproduce the bug and record expected and actual results before editing.
  • A useful hypothesis predicts something you can inspect.
  • Fix the cause with one change, then retest failing and previously working cases.
  • Use a minimal example or intermediate values to narrow the search.
  • Check types and hidden characters when values look misleading.

3.2 Watch values change with prints and a debugger

A program's final answer hides the path that produced it. Temporary prints, a trace table and a debugger let you see that path and check a specific hypothesis about where it goes wrong.

Print a value at a known moment

Label diagnostic output so a number has a clear meaning. Python 3.8 and later support this f-string form:

print(f"{i=} {total=}")

For the buggy price loop, put it immediately before total += prices[i]. With prices [4, 6, 10], it prints:

i=1 total=0
i=2 total=6

These are totals before each addition. After the first print, the loop adds 6; after the second, it adds 10. A print after the addition would show different totals. Decide which moment answers your question before interpreting the output.

Trace the same code by hand

A trace table records variables step by step. For this loop:

Index iPrice addedTotal beforeTotal after
1606
210616

There is no iteration for index 0. Follow the actual start and stop values written in the code, even when you already know what the loop ought to do. A table helps separate what you expect from what happens.

Pause at a breakpoint

A debugger pauses a running program to let you inspect its current state. A breakpoint marks a place to pause. In editors such as Thonny and VS Code, click beside a line number and start debug mode; in IDLE, right-click the line and choose Set Breakpoint.

After pausing, choose a step based on the question you are investigating:

ControlEffect
Step overRun the current line, including a called function
Step intoEnter a function called on that line
Step outFinish this function and pause after returning
ContinueRun until another breakpoint or the program ends

If the caller passes correct arguments but receives a wrong answer, step into the function. If you only need to see the returned result, step over its call. Inspect the values before and after the line that might create the error.

Use Python's text debugger

Insert breakpoint() just before the code you want to inspect. With the default debugger configuration, running the program starts pdb at that point. Type these commands at its (Pdb) prompt:

  • p total displays total.
  • n steps over the next line.
  • s steps into a function call.
  • c continues execution.
  • q quits the debugging session.

These are debugger commands, not lines to copy into your program.

Separate printed output from returned results

A function can print the right number and still return None because it lacks return total. Inspect the value received by the caller when the failure occurs after the function call. Seeing correct output inside the function does not establish that the caller received that number.

Use prints for quick evidence across loop iterations and the debugger to inspect a particular moment. After repairing the bug, remove temporary prints and breakpoint() calls so the finished program runs normally.

Key Takeaways

  • Label debugging prints and note whether they run before or after an update.
  • A trace table follows actual variable changes one step at a time.
  • Breakpoints pause execution; stepping controls how much code runs next.
  • breakpoint() opens pdb by default; p, n, s, c and q are debugger commands.
  • Inspect returned values when a function prints correctly but its caller fails.
  • Remove temporary diagnostic output and breakpoints after the fix.
4

Check your functions with tests

Automated checks let you repeat a useful example every time a function changes. You will write assertions with clear expectations, choose inputs that reach the edges of a rule, and keep tests from starting the application they import. The standard unittest framework then gives each test a name and a separate result. Reading its failures and errors connects testing to debugging: the test reproduces a problem, and investigation finds its cause.

4.1 Write checks that fail when an assumption is false

An assertion states something that must be true if your code is working correctly. It can stop a bug close to where it begins or compare a function's result with the value you expect.

Write a condition and an optional message
assert letter_grade(90) == "A", "90 should be an A"

Python evaluates the condition. If letter_grade(90) returns "A", the condition is true and execution continues without output. If it returns "B", Python raises AssertionError with the message 90 should be an A. The traceback points to the failing check.

Without the message, assert letter_grade(90) == "A" still checks the result. The final exception line contains AssertionError, while the traceback shows the expression. A useful message names the expectation that failed; it should help you locate the rule being checked.

Stop an internal mistake early

Suppose an internal calculation requires a nonempty list:

def average(values):
    assert len(values) > 0, "values must not be empty"
    return sum(values) / len(values)

An empty list now triggers the assertion before the division. The check documents the function's internal assumption and points directly to its violation. It does not give an empty list an average or decide how an application should respond to a user's missing data.

Keep user validation active

Python's -O option removes assert statements. A program that relies on an assertion to reject outside input can therefore behave differently when optimized. Check user input and other outside data with ordinary control flow:

def require_score(score):
    if score < 0 or score > 100:
        raise ValueError("score must be 0 to 100")
    return score

Here raise ValueError(...) reports an unsuitable value. The if statement still runs under -O. Choose an assertion for an internal programming assumption and an active input check for data that the outside world can get wrong.

Avoid a check that can never fail

assert is a statement, not a function. Write the condition and message separated by a comma:

assert x > 10, "x too small"

Writing assert (x > 10, "x too small") creates a two-item tuple. A nonempty tuple is true even when its first item is False, so the assertion passes. Python can warn that this assertion is always true. Parentheses around just a condition, as in assert (x > 10), do not create that tuple; the comma inside the parentheses is the problem.

Use == for comparison. assert total = 20 is invalid syntax because = assigns a value.

Compare numbers with the intended precision

For a calculated float, use a closeness check:

import math

assert math.isclose(0.1 + 0.2, 0.3)

An exact comparison fails here because the stored float sum differs slightly from 0.3. A simple automated test applies the same assertion idea to a function: assert total_price([4, 6, 10]) == 20. The expected value comes from the function's rule, so the check can expose a wrong implementation.

Key Takeaways

  • assert condition, message raises AssertionError when its condition is false.
  • Assertions can check internal assumptions and expected function results.
  • Use if and an error message or exception for outside input; -O removes assertions.
  • A comma inside assert (condition, message) creates a truthy tuple.
  • Use == for equality and math.isclose for calculated float comparisons.

4.2 Choose tests that can expose a bug

A unit test checks a small unit of code, usually one function, with a chosen input and an expected result. Automated tests make the same checks easy to repeat after edits, helping catch bugs that return in previously working behavior.

Start with the specification

Suppose a grading function must return "A" for scores of at least 90, "B" for scores of at least 80 but below 90, and "C" below 80. The buggy first condition is score > 90.

Tests should include several jobs:

  • Typical cases check ordinary inputs inside each region, such as 95 and 85.
  • Boundary cases check the values where a rule changes, such as 90 and 89, or 80 and 79.
  • Edge cases check inputs such as zero or an empty collection when the function's rules allow them.

Work out the expected answer from those rules. Copying the current program's answer into a test can preserve its mistake.

Put the tests beside the module

Save tests in test_grade.py and the function in grade.py. The application launch in grade.py belongs under its __main__ guard, so importing the function does not start the program.

from grade import letter_grade

def test_letter_grade():
    assert letter_grade(95) == "A"
    assert letter_grade(90) == "A"
    assert letter_grade(85) == "B"

For the buggy rule, the first assertion passes. The second fails because 90 is not greater than 90; execution stops there, before the third assertion in this function. Changing the first condition to score >= 90 repairs this boundary. Rerun all tests to check the affected case and the earlier behavior.

Make sure the tests actually run

Defining a test function does not call it. With no test runner, add test_letter_grade() at the bottom of the test file, then run python test_grade.py. To use pytest, install it with python -m pip install pytest, then run pytest test_grade.py in the terminal. It finds the test functions and reports their results. Keep manual calls out when letting a runner manage them.

A test that merely calls letter_grade(90) has no comparison. It can finish successfully while the returned answer is wrong; it fails only if the call raises an unexpected exception. An assertion connects the call to the required behavior.

Test returned values and random properties

A function that only prints a total returns None. A comparison such as assert print_total([4, 6]) == 10 therefore fails even if the screen displays 10. Have the calculation return its answer, then let main handle printing.

Random results need a different expectation. An unseeded die function cannot be required to return one exact face on every call. Check that each of 100 calls returns an allowed value from 1 through 6, or seed before a repeatable sequence. Passing the bounds test checks the bounds for those calls; it does not establish that every face is equally likely.

When a new bug appears, add a test that fails because of it before repairing the function. Keep that regression test so later edits check the same behavior again. Passing tests establish what happened for the tested cases; more inputs can still reveal another bug.

Key Takeaways

  • Choose expected results from the specification, independently of the implementation.
  • Test ordinary inputs, rule boundaries and relevant edge cases.
  • Test functions need a call or a runner; definitions alone perform no checks.
  • Assertions must compare returned results with the intended behavior.
  • Test appropriate properties of random output or arrange a repeatable sequence.
  • Keep a test for each newly found bug to help catch regressions.

4.3 Run and interpret a unittest test file

Python includes unittest, a test framework that discovers tests, runs them and reports separate outcomes. It needs no pip installation. Its class structure can be used as a template while you are learning to test functions.

Write independent test methods

For a function in grade.py, put this in test_grade.py:

import unittest
from grade import letter_grade

class TestLetterGrade(unittest.TestCase):
    def test_top(self):
        self.assertEqual(letter_grade(95), "A")

    def test_boundary(self):
        self.assertEqual(letter_grade(90), "A")

    def test_low(self):
        self.assertEqual(letter_grade(50), "C")

if __name__ == "__main__":
    unittest.main()

TestLetterGrade inherits the test tools from unittest.TestCase. Each test is a method whose name starts with test. self identifies the current test object; keep it in each method's parameter list and before assertion method calls. You can copy this structure without designing classes of your own.

Tests run in alphabetical order of their method names by default, so test_boundary comes before test_low and test_top. Make every test supply its own needed input and work independently of previous tests.

Pick an assertion method
MethodCheck
self.assertEqual(actual, expected)Values are equal
self.assertNotEqual(a, b)Values differ
self.assertTrue(condition)A condition is true
self.assertFalse(condition)A condition is false
self.assertIn(item, collection)The item belongs to the collection
self.assertAlmostEqual(a, b)The difference rounds to zero at 7 decimal places by default

Prefer assertEqual(a, b) to assertTrue(a == b) when comparing values: the failure report shows the two values. The unittest methods also keep their checks when Python runs under -O.

To check an expected exception, enclose the call in assertRaises:

def test_score_too_high(self):
    with self.assertRaises(ValueError):
        letter_grade(120)

This test requires the function's rule to reject that score with ValueError. A return without an exception fails this check; a different non-assertion exception is an error.

Run and read the report

Run python test_grade.py to execute its guarded unittest.main(). Or run python -m unittest for discovery of test*.py files in the current folder. python -m unittest -v prints individual test names and their outcomes.

In the short report, . means pass, F means an assertion failed and E means another exception occurred. For a grading function with score > 90, the three tests above produce F... The failure report for test_boundary shows 'B' != 'A': the actual result is "B", while the expected result is "A". The summary reports three tests and one failure.

Before editing the function, check the test's expectation against the grading rules. A wrong expectation needs a test fix. If no tests run, inspect the method names, the direct-run guard and the launch command. Also avoid naming your file unittest.py, which hides the standard module.

Key Takeaways

  • Put test methods inside a class that inherits from unittest.TestCase.
  • Use self.assertEqual and other assertion methods for clear checks and reports.
  • assertRaises checks an expected exception; float comparisons can use assertAlmostEqual.
  • Run a guarded test file directly or use python -m unittest for discovery.
  • Read F as a failed assertion and E as another exception.
  • Check a failed test against the specification before changing the function.
  • Each test must run independently of alphabetical execution order.

Python Modules and Debugging examples

4 Walkthroughs

1

Build a program from reusable pieces

20 steps
Choose imports and library functions, reuse your own code, and connect installed packages to the Python that runs them.
  1. 1
    IntroductionStart with code you can reuse

    A program can call code from Python's standard library, another file you wrote, or an installed package. The first question is what name the program has available after importing.

    You will follow a calculation from an import to its returned value, then arrange a reusable module and connect a package installation to its interpreter. Along the way, check whether a result is a value you keep or a change made to an existing list.

  2. 2
    ExplanationThe import chooses the call style

    These imports expose the same function through different bindings:

    import math
    print(math.sqrt(16))
    
    from math import sqrt
    print(sqrt(16))
    

    Both print 4.0. The first creates math, so the call uses a dot. The second creates sqrt, so it needs no module prefix. Using the wrong call style raises NameError when that name has not been bound.

  3. 3
    ExampleCalculate the hypotenuse

    With import math, a right triangle with sides 3 and 4 can use:

    length = math.sqrt(3**2 + 4**2)
    

    The squares are 9 and 16. Their sum is 25, and math.sqrt(25) returns 5.0. Python evaluates the argument before calling the function.

    The prefix also tells a reader where sqrt came from. A wildcard import hides that origin and can replace existing names, such as a variable named e.

  4. 4
    ChallengeFollow the alias binding

    A fresh script runs these lines. What happens on the final line, and how can it be repaired?

    import statistics as st
    print(statistics.mean([4, 8, 12]))
    

    ---reveal---

    It raises NameError because the import binds st, not statistics. Use st.mean([4, 8, 12]), which returns 8. An alias selects the binding in this program; it does not also create the original module name.

  5. 5
    ExampleRound for enough whole containers

    A wall needs paint for 23 square meters. Each can covers 10 square meters.

    import math
    cans = math.ceil(23 / 10)
    

    Division gives 2.3; ceil gives 3, enough whole cans. round(2.3) gives only 2.

    For negative inputs, down still means toward negative infinity: math.floor(-2.5) is -3, while int(-2.5) cuts toward zero and gives -2.

  6. 6
    Key InsightMatch the comparison to the number

    Calculated floats can differ slightly from their decimal-looking targets:

    0.1 + 0.2 == 0.3               # False
    math.isclose(0.1 + 0.2, 0.3)   # True
    

    Use closeness for that approximate comparison. Separately, a function's allowed inputs still matter: math.sqrt(-1) raises ValueError.

    The built-in round needs no import. At an exact halfway value it chooses the even neighbor: round(2.5) is 2, and round(3.5) is 4.

  7. 7
    ExplanationPick the random result you need

    After import random, choose a call by its job:

    • randint(1, 6) picks an integer including 6.
    • randrange(1, 6) picks an integer excluding 6.
    • random() gives a float from 0.0 up to, but excluding, 1.0.
    • choice(items) picks one item.
    • sample(items, k) picks k positions without replacement.

    Two selected positions can hold the same value. Sampling [1, 1] at both positions therefore gives repeated 1s.

  8. 8
    ChallengeKeep the shuffled list

    What value does names hold at the end, and how should the last line change to keep the list?

    names = ["Mina", "Luis", "Jo"]
    names = random.shuffle(names)
    

    ---reveal---

    It holds None. shuffle changes the existing list, then returns None; the assignment replaces the list binding with that return value. Use random.shuffle(names) on its own line. The list's new order is random, but its return behavior is fixed.

  9. 9
    ConnectionRepeat a simulation and find other tools

    Call random.seed(42) before generating values to repeat the same sequence on the same Python version. That makes a random failure easier to reproduce. Use secrets for security tokens.

    Other standard modules solve everyday jobs: statistics.mean([2, 4, 9]) gives 5, and median gives 4. datetime.date.today() gives today's date. sys.argv lists the running script's name followed by its command-line arguments.

  10. 10
    ExamplePut a reusable function in a module

    Save this as geometry.py:

    import math
    
    def circle_area(radius):
        """Return the circle's area."""
        return math.pi * radius ** 2
    

    A neighboring script can run import geometry, then print(geometry.circle_area(2)). It prints 12.566370614359172.

    Import the file without .py. The function's first string is its docstring, which help(geometry.circle_area) can display.

  11. 11
    ExplanationAn import executes top-level statements

    Suppose geometry.py ends with print(circle_area(2)), outside a function. Its first import prints the area as a side effect.

    Python runs module-level statements in order. Defining circle_area creates the function without running its body; calling it inside the top-level print runs the calculation. A second import in the same process reuses the loaded module. An interactive shell also keeps that loaded version after the file is edited, until you restart it.

  12. 12
    ExampleLaunch only during a direct run

    Leave the reusable function above this block:

    def main():
        print(circle_area(2))
    
    if __name__ == "__main__":
        main()
    

    A direct run sets __name__ to "__main__", so main() runs. An import sets it to "geometry", so the launch is skipped.

    The definitions stay available in either case. This lets a test import circle_area without producing the program's ordinary output.

  13. 13
    ChallengeKeep the function available to an importer

    A file defines convert inside its if __name__ == "__main__": block. A fresh test process imports the file and cannot find convert. What arrangement fixes both reuse and the direct-run launch?

    ---reveal---

    Move def convert(...) outside the guard and keep only the program launch inside it. During import the guard is false, so a definition inside it is skipped. Moving the definition makes it available without starting the application.

  14. 14
    TipCheck the file that an import finds

    Python checks built-in modules, then searches sys.path in order. The script's folder normally appears before library folders. A local random.py can therefore hide the random you intended to import.

    Rename that file to something such as dice.py. For your own helpers, use valid names such as my_tools.py and keep them beside the beginner script that imports them. A file outside the search path is not found by an ordinary import.

  15. 15
    ExampleInstall in the terminal, import in Python

    In the terminal, run:

    python -m pip install cowsay
    

    Then Python code can use:

    import cowsay
    cowsay.cow("Hello, Ada")
    

    The package prints a text cow and speech bubble. pip installs code, while import creates the binding used by your program. The installation command is not a Python statement; at a >>> prompt it produces invalid syntax.

  16. 16
    ExplanationUse pip belonging to the running Python

    If a script runs with python3, install through the same command:

    python3 -m pip install cowsay
    python3 game.py
    

    -m pip selects pip belonging to that interpreter. On Windows, your command may instead be py.

    If the editor cannot import a successfully installed package, compare its selected interpreter with the one used in the terminal. Installation success in one Python does not install the package into every other Python.

  17. 17
    ChallengeSeparate the install name from the import name

    A package's documentation says to install Pillow and import PIL. The install succeeds for your running Python, but import Pillow fails. What should change, and why?

    ---reveal---

    Use import PIL. The distribution name used by pip can differ from the Python import name it provides. Since the interpreter match is already established, repeating the unchanged installation command would leave this particular naming mistake in place.

  18. 18
    TipInspect a package before changing it

    python -m pip list shows installed versions. show requests gives one package's version and location; install --upgrade requests updates it, and uninstall requests removes it. Each short command follows python -m pip.

    Check the exact PyPI name and project documentation before installing. For an externally-managed-environment error, use a project virtual environment; do not force an installation with sudo pip install.

  19. 19
    ChallengePredict an import and a later call

    A fresh module sizes.py contains this code. Another script runs from sizes import batches, then print(batches(17)). What prints?

    import math
    
    def batches(n):
        return math.ceil(n / 5)
    
    if __name__ == "__main__":
        print(batches(12))
    

    ---reveal---

    Only 4 prints. Import creates the function but skips the guarded print. The later call divides 17 by 5, then rounds 3.4 up to 4. The math binding inside sizes is available to its function.

  20. 20
    RecapFollow code from its source to the call

    Choose an import that matches the names you want to use. Check what a library call returns, including a possible None from an operation that changes a list.

    Keep reusable definitions available during import and place the application launch behind the main guard. For installed code, match pip to the running interpreter and check the documented import name. When a call fails, these details help separate a missing binding, a missing attribute and a missing installation.

2

Read a failure back to its cause

11 steps
Read call frames, recognize common exceptions, and separate a crash from a wrong result that gives no error message.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionUse the report as evidence
  2. 2
    ExampleAn empty list reaches a division
  3. 3
    ExplanationFind the most recent frame
  4. 4
    ExampleFollow the argument back through its callers
  5. 5
    ChallengeLocate an unsuitable argument
  6. 6
    ExplanationDistinguish grammar, types and values
  7. 7
    ExplanationDifferent lookups have different errors
  8. 8
    ExampleA wrong total can finish normally
  9. 9
    ChallengeExplain a failed addition
  10. 10
    ChallengeUse a library frame without blaming the library
  11. 11
    RecapChoose the next place to look
3

Investigate one bug from evidence to repair

14 steps
Reproduce a wrong total, inspect its loop, make a focused fix, and use debugger controls to follow a function call.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionMake the wrong result repeat
  2. 2
    ExampleState expected and actual results
  3. 3
    ExplanationMake the guess testable
  4. 4
    ExamplePrint before the update
  5. 5
    ExampleTrace the additions by hand
  6. 6
    ChallengeInterpret a print after the update
  7. 7
    ExampleRepair the iteration itself
  8. 8
    ConnectionRetest inputs with different shapes
  9. 9
    TipShrink a larger failure
  10. 10
    ChallengeCheck a value that looks numeric
  11. 11
    ExplanationPause at the call you need to inspect
  12. 12
    ChallengeChoose the stepping command
  13. 13
    ChallengeInvestigate a correct print and a wrong return
  14. 14
    RecapKeep the repair tied to evidence
4

Turn a boundary bug into a lasting test

16 steps
Write assertions, choose revealing inputs, and use unittest to run independent checks and interpret their outcomes.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionGive expected behavior a repeatable check
  2. 2
    ExplanationAn assertion states an expectation
  3. 3
    ChallengeFind a check that cannot fail
  4. 4
    Key InsightKeep outside-input checks active
  5. 5
    ExampleChoose inputs around the grade change
  6. 6
    ExampleWrite a test function that imports the code
  7. 7
    TipExecute the checks you defined
  8. 8
    ChallengeRepair a threshold and preserve the check
  9. 9
    ConnectionTest the returned result or a valid property
  10. 10
    ExampleUse the unittest class template
  11. 11
    ExplanationPick the assertion for the outcome
  12. 12
    ExampleRead a failed grade check
  13. 13
    ChallengeDistinguish a failure from an error
  14. 14
    TipCheck what discovery actually ran
  15. 15
    ChallengeUse a report to plan the repair
  16. 16
    RecapKeep a test for the behavior you repaired

Python Modules and Debugging practice questions

40 Practice Questions in 4 sets

Set 1: Imports, error clues and first checks10 Practice Questions

Question 1 of 10

A script contains only import math before its calculation. How should it call the square-root function on 81?
  1. sqrt(81)
  2. math(81).sqrt()
  3. math.sqrt(81)
  4. import.sqrt(81)
Show answerHide answer
Answer: C
The import creates the binding math, so the function is reached through math.sqrt(81), which returns 9.0. Calling sqrt(81) alone would raise NameError because this import did not create a separate binding for sqrt.

Question 2 of 10

A shuttle holds 8 people. What does math.ceil(35 / 8) return, and what does it represent?
  1. 5, the shuttles needed for everyone
  2. 4, the shuttles needed for everyone
  3. 4.375, the shuttles you must book
  4. 4, the number of seats left empty
Show answerHide answer
Answer: A
Division gives 4.375, so four shuttles leave three people without seats. ceil rounds up to the next integer, giving five shuttles. Rounding to the nearest integer would give four here, but the requirement is enough whole shuttles for everyone.

Question 3 of 10

A file called text_tools.py defines shorten. How can another file in the same folder import and call it?
  1. import text_tools.py, then text_tools.shorten("Hello")
  2. import shorten, then shorten.text_tools("Hello")
  3. import text_tools, then shorten.text_tools("Hello")
  4. import text_tools, then text_tools.shorten("Hello")
Show answerHide answer
Answer: D
A file's module name leaves off the .py extension, so import text_tools creates the module binding. Dot notation then reaches its shorten function. Including .py in the import treats the ending as part of an import path instead of a file extension.

Question 4 of 10

You are at a Python >>> prompt and enter pip install cowsay. Why does Python report invalid syntax?
  1. The package must be imported before it can be installed.
  2. The installation command belongs in the terminal.
  3. The package name must be written with a .py ending.
  4. The installation command needs a Python assignment.
Show answerHide answer
Answer: B
The >>> prompt accepts Python statements, while pip installation syntax is a terminal command. Leave the Python shell and run python -m pip install cowsay in the terminal. Importing first cannot make a not-yet-installed package available.

Question 5 of 10

A traceback ends in ZeroDivisionError. Its frames, top to bottom, show show([]), mean(data), and sum(values) / len(values). What chain best explains the failure?
  1. The show function tried to divide its own name by zero.
  2. An empty list reached mean, making its divisor zero.
  3. The list became empty because the traceback prints backward.
  4. The sum was zero, which always makes division invalid.
Show answerHide answer
Answer: B
The most recent frame divides by len(values), and the earlier calls trace that value back to the empty list. Its length is zero, causing the exception. A zero numerator is allowed when the divisor is nonzero, so the sum alone does not explain this crash.

Question 6 of 10

What exception does int("7.25") raise?
  1. TypeError
  2. NameError
  3. IndexError
  4. ValueError
Show answerHide answer
Answer: D
The string is an accepted input type for int, but "7.25" is not text in the whole-number form that this conversion requires. That makes it a ValueError. A TypeError would indicate an unsuitable input type or operation, not this unsuitable string value.

Question 7 of 10

A function should total [9, 2, 5] as 16 but returns 7. You suspect it skips the first item. What observation would most directly test that idea?
  1. Record the indexes visited by the summing loop.
  2. Print the expected total again after the function call.
  3. Add 9 to the result and check only this same input.
  4. Rename the total variable and rerun without inspection.
Show answerHide answer
Answer: A
Recording the indexes checks the prediction that index 0 is never visited. If only indexes 1 and 2 appear, that supports the hypothesis and points to the loop bounds. Adding 9 patches this result but gives no evidence about whether the loop processes other inputs correctly.

Question 8 of 10

What two lines are printed?

total = 0
for n in [3, 8]:
    print(f"{n=} {total=}")
    total += n
  1. n=3 total=3, then n=8 total=11
  2. n=3 total=0, then n=8 total=0
  3. n=3 total=0, then n=8 total=3
  4. n=0 total=3, then n=3 total=8
Show answerHide answer
Answer: C
The print runs before the addition on each iteration. It first shows the starting total of 0, then the total of 3 left by the first addition. Showing totals 3 and 11 would describe a print placed after each update, which is not where this print appears.

Question 9 of 10

What happens when this code runs normally?

count = 6
assert count < 5, "too many"
print("saved")
  1. It raises AssertionError with "too many".
  2. It prints "saved" and then reports the assertion.
  3. It assigns 5 to count and continues without output.
  4. It prints "too many" and then prints "saved".
Show answerHide answer
Answer: A
The condition 6 < 5 is false, so the assertion raises AssertionError with the supplied message. Execution stops before the final print because the exception is unhandled. The message is part of the exception; it is not an ordinary print followed by continued execution.

Question 10 of 10

The rule is free through weight 12, a fee of 4 for 13 through 18, and a fee of 7 above 18. Tests for 5 and 20 pass. What results and edit correctly diagnose this code?

def fee(weight):
    if weight < 12:
        return 0
    if weight <= 18:
        return 4
    return 7
  1. At 12: 0; at 18: 7. Change the second test to < 18.
  2. At 12: 4; at 18: 7. Change both comparisons to <.
  3. At 12: 0; at 18: 4. Keep both comparisons unchanged.
  4. At 12: 4; at 18: 4. Change the first test to <= 12.
Show answerHide answer
Answer: D
At 12, the first condition is false, so the second branch wrongly charges 4. At 18, that second branch correctly charges 4. Including 12 in the first branch repairs the free limit; changing the second comparison would create another boundary error. The passing distant tests do not check either boundary.
Set 2: Modules, debugging choices and test results10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

After import statistics as st, the next line is statistics.mean([3, 7]). No other imports or assignments appear. What explains the error?
  1. The mean function requires import math as well.
  2. Only st was bound; call st.mean([3, 7]).
  3. Only mean was bound; call mean([3, 7]).
  4. The alias must be passed as statistics.mean(st).
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What is the value of tickets after this code?

import random
tickets = [11, 22, 33]
tickets = random.shuffle(tickets)
  1. A newly shuffled three-item list
  2. The first item of the shuffled list
  3. The original list in its original order
  4. None
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A helper module defines double and then has the unguarded line print(double(6)). Another script imports it twice in one run. How many times does that line print?
  1. Once, during the first import
  2. Twice, once for each import statement
  3. Never, because imports only read function names
  4. Once, only if the script later calls double
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An editor runs a script with Python A. The terminal command python -m pip show cowsay reports a location in Python B, and the editor cannot import cowsay. What should you do first?
  1. Run Python B's pip installation again unchanged.
  2. Replace the import with the package's folder path.
  3. Run Python A's -m pip install cowsay.
  4. Move the script into Python B's library directory.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A traceback has three frames. Where do you first find the exception type and message?
  1. In the first frame, by the script name
  2. In the header above the first frame
  3. On the line with the oldest call
  4. On the last line, below the frames
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

This function should add every quantity. What kind of problem explains the result of total([2, 5, 8])?

def total(values):
    result = 0
    for i in range(1, len(values)):
        result += values[i]
    return result
  1. An IndexError occurs because the loop reaches index 3.
  2. A logic error returns 13 because index 0 is skipped.
  3. A TypeError occurs because list items cannot be added.
  4. A syntax error occurs because range needs three arguments.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A failing data-processing program has 300 lines and 200 records. What makes a useful minimal reproducible example?
  1. The full code with every record and all diagnostic output
  2. The expected answer with the failing input removed entirely
  3. The shortest code and smallest input that still fail
  4. A rewritten program whose output has already been corrected
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

An input check uses assert (score >= 0, "negative score"). It passes when score is -3. What change both repairs the check and keeps validation active under -O?
  1. if score < 0: raise ValueError("negative score")
  2. assert score >= 0, "negative score"
  3. assert (score < 0, "negative score")
  4. assert (score >= 0) == True
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A plain test file defines test_discount, which contains two assertions, but never calls it. You run that file with python test_discount.py; there is no test runner code. Why are no assertions checked?
  1. Python removes every assertion from any test_ file.
  2. Each assertion needs to be moved above the function.
  3. Defining the function does not execute its body.
  4. The test function must return True before being called.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A unittest method calls self.assertEqual(lookup("x"), 7). The call to lookup raises an unexpected KeyError before returning. What short result marks this test?
  1. F, because every problem in a test is an assertion failure
  2. E, because another exception interrupted the check
  3. ., because no equality comparison returned false
  4. F, because KeyError is the same as AssertionError
Show answerHide answer

Save this lesson to see this answer.

Set 3: Trace causes across modules and tests10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What does this code print?

from math import sqrt as root
sqrt = 4
result = root(sqrt ** 2)
print(result, sqrt)
  1. 16.0 4
  2. 4.0 4.0
  3. It raises TypeError because sqrt is an integer.
  4. 4.0 4
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A draw uses random.sample(["blue", "blue", "red"], 2). Why can the result contain two "blue" values?
  1. Sampling always replaces the first item before drawing again.
  2. The two blue entries occupy different input positions.
  3. The value 2 requests two identical copies of a chosen item.
  4. Sampling removes duplicate values only after returning them.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A module contains this code. A fresh process imports it and then calls helper.answer(). What happens?

# helper.py
if __name__ == "__main__":
    def answer():
        return 42
  1. It returns 42 because Python loads all definitions first.
  2. NameError: Python never defines __name__ during import.
  3. AttributeError: importing skipped the function definition.
  4. It returns None because the guard changes return values.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A traceback starts with your run_report() call, then your parse(raw) call, and ends inside a library conversion with ValueError. You inspect raw at your parse call and find "9x", although the rule requires whole-number text. Where should the investigation go next?
  1. Trace where raw became "9x" before parse was called.
  2. Replace int(raw) with int(raw[0]) so this input produces a number.
  3. Treat the library frame as proof the library is faulty and edit its conversion.
  4. Keep the input unchanged and reinstall the library because the caller already ran.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A function prints 18 but has no return statement. Its caller then evaluates result + 2, where result is the call's return value. Why does a TypeError occur?
  1. result is None, which cannot be added to 2.
  2. result is the string "18" because printing converts returns.
  3. result is 18, but adding two integers is invalid here.
  4. result is undefined because every print deletes its binding.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A pipeline reads, converts and totals data. For a repeatable failure, the values needed for the total are already wrong immediately after conversion. Reading alone produces the expected raw values. What is the most focused next investigation?
  1. Add a correction to the final total and leave conversion unchanged.
  2. Rewrite all three stages together and compare only the final output.
  3. Run the conversion alone on the smallest failing raw input.
  4. Inspect only the final print because it is where wrong output appears.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A debugger is paused on answer = calculate(7). You want to inspect the first line inside calculate. What control should you use?
  1. Step over
  2. Step into
  3. Step out
  4. Continue
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

The rule says a trip is free for distances through 6 and costs 3 above 6. charge(6) returns 3, but its test expects 3 and passes. What change creates a useful regression test and fixes the rule?
  1. Keep 3 at 6, then add only a test for a distant paid trip.
  2. Change the rule to make 6 paid, then keep the existing test.
  3. Remove the check at 6, then compare only two passing examples.
  4. Expect 0 at 6, see it fail, then fix and rerun the tests.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A unittest file has test_alpha expecting 4 but receiving 5, test_beta passing, and test_gamma raising an unexpected TypeError. What compact progress string does default alphabetical execution produce?
  1. E.F
  2. F.E
  3. F.F
  4. .FE
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A unittest.TestCase has check_empty containing a failing equality check and test_full containing a passing one. The runner reports one test and OK. How should you make the missing behavior check run?
  1. Rename check_empty to test_empty and rerun.
  2. Add return False at the end of check_empty.
  3. Move check_empty below test_full in the class.
  4. Replace assertEqual with print in check_empty.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Combine import, debugging and testing decisions10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A program has assert total == 10 for an internal calculation and assert age >= 0 for user input. It runs under -O with total equal to 9 and age equal to -1. What happens to those checks, and what change keeps the required input validation?
  1. Only the total check disappears; give the age assertion a longer message.
  2. Both checks disappear; put the age condition and message inside a tuple.
  3. Both checks disappear; use if age < 0 and an error response for the input.
  4. Only the age check disappears; move it into a function named test_age.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A simulation seeds once, then calls random.randint(2, 5) 50 times. A new test seeds to the same value on the same Python version and repeats the same calls. What should its checks expect?
  1. The sequence repeats, and every result lies from 2 through 5.
  2. The sequence repeats, and every result lies from 2 through 4.
  3. Each result occurs equally often, and all four values must appear.
  4. Every call gives the first result again because the seed stays set.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

The documentation names beautifulsoup4 as the distribution and bs4 as its module. A script imports bs4 successfully. A coworker wants to change python -m pip install beautifulsoup4 to install bs4 so the names match. Is matching those names a sound reason for that change?
  1. Yes; every successful import must use the distribution's installation name.
  2. Yes; importing bs4 proves that pip installed a distribution under that name.
  3. No; installation follows the documented distribution name, which can differ.
  4. No; the module must first be renamed to match the current distribution.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A fresh run of display.py reports these calls, top to bottom: start(" "), read_count(text), then int(text), ending in ValueError. What sequence best tests the likely cause before a fix?
  1. Inspect only type(text); receiving a string is enough to establish a type error.
  2. Inspect repr(text), confirm the blank entry, then define its input behavior.
  3. Move the same conversion to start so it runs before the other function calls.
  4. Replace every failed conversion with zero, without checking the required input behavior.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A total function skips the first item. A patch adds 6 to every returned total and now passes [6, 3, 2]. What expected and actual results for two new inputs expose the remaining problem?
  1. [9]: expected 9, actual 15; []: expected 0, actual 6.
  2. [9]: expected 9, actual 6; []: expected 0, actual 0.
  3. [9]: expected 6, actual 6; []: expected 6, actual 6.
  4. [9]: expected 9, actual 6; []: expected 0, actual 6.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

This function prints a correct product, but its caller gets a TypeError in area(4, 5) + 1. Where should you inspect, and what result explains the failure?

def area(w, h):
    product = w * h
    print(product)
  1. Step over the call; its returned value is the integer 20.
  2. Step over the call; its returned value is the string "20".
  3. Step over the call; its returned value is None.
  4. Step into the call; its printed result becomes the return value.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

What happens under an ordinary, unoptimized run?

import math
assert math.isclose(0.1 + 0.2, 0.3)
assert 0.1 + 0.2 == 0.3
  1. The first assertion passes; the second raises AssertionError.
  2. Both assertions pass because decimal sums are stored exactly.
  3. The first assertion fails; the second never executes.
  4. Both assertions fail but the program continues to the end.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

pick() returns a random integer from 3 through 7, including both endpoints. pair_total() adds two calls to pick(). A test asserting that every unseeded total equals 10 sometimes fails even though both functions work correctly. What per-result check matches the required total range?
  1. Assert 3 <= value <= 7 for each total.
  2. Assert 6 <= value < 14 for each total.
  3. Assert value == 10 for each total.
  4. Assert 6 <= value <= 14 for each total.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

reserve(n) must accept whole-number counts from 0 through 8 and raise ValueError outside that range. Tests using assertRaises(ValueError) report F for -1 because no exception occurs, and E for 9 because a TypeError occurs. A test at 2 passes. What behavior should the repair provide?
  1. Accept -1 normally; raise ValueError at 9; keep 2 valid.
  2. Raise ValueError at -1 and 9; keep 2 valid.
  3. Raise ValueError at -1; keep TypeError at 9; keep 2 valid.
  4. Accept -1 and 9 normally; raise ValueError at 2.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A script is named random.py and imports random, then fails because random.randint is missing. What first change addresses the name clash?
  1. Install the random module again using pip.
  2. Move import random below the call to randint.
  3. Keep random.py and use from random import randint.
  4. Rename the script to draw_numbers.py.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

60 Key Terms, each with its definition

A

Alias
An alias is the name chosen with as for an imported module or object; the import binds that name in the importing program.
The alias stats in import statistics as stats lets the script call stats.mean([1, 3, 8]).
Assertion
An assertion checks a condition expected to hold if code is correct, continuing when true and raising AssertionError when false.
An assertion such as assert count == 3 checks an internal result against the count you independently expect.
AssertionError
An AssertionError signals a failed assertion and may include a supplied message explaining which expectation the check did not meet.
An AssertionError with "wrong total" directs attention to the test that compared the calculated total.

B

Boundary case
A boundary case is a test input at an edge where a rule changes, making inclusive and exclusive comparison mistakes easier to detect.
A boundary case at score 90 checks whether the A-grade rule uses the required inclusive comparison.
Breakpoint
A breakpoint marks a location where a debugger pauses execution, allowing inspection before continuing with the following code.
A breakpoint before a loop update lets you compare the current total with the item about to be added.
Bug
A bug is a mistake that causes program behavior to differ from its intended behavior, including wrong answers and unexpected failures.
A bug that skips the first price can be reproduced with a one-item list whose expected total is easy to check.

C

Call chain
A call chain is the sequence of active function calls that led to the current operation, from the outer caller to the most recent call.
A call chain from a report function to an average function shows how a list reached the function that divided by its length.
Ceiling
The ceiling of a number is the smallest integer no smaller than that number; math.ceil gives this upward integer rounding.
A ceiling calculation turns 3.4 required paint cans into four whole cans, enough for the stated area.

D

Debugger
A debugger pauses program execution and provides controls for inspecting current values and advancing through code a step at a time.
A debugger lets you enter calculate to inspect its arguments when the caller receives a wrong answer.
Debugging
Debugging is the process of reproducing unexpected behavior, locating its cause, repairing it and checking the affected behavior again.
Debugging a loop that totals prices includes observing the indexes it visits before changing the loop's start value.
Distribution
A distribution is an installable Python project identified by a package-manager name, which may differ from the name used in imports.
The distribution beautifulsoup4 is installed by that name, but its documentation tells Python users to import bs4.
Divide and conquer
Divide and conquer narrows a debugging search by checking needed intermediate values and following the section where they first go wrong.
Divide and conquer directs attention to conversion if raw data is correct but converted values are already wrong.
Docstring
A docstring is a string at the start of a module or function that documents its purpose and can be displayed through help().
A docstring on circle_area can explain the radius argument and the area returned to its caller.
Dot notation
Dot notation reaches a named attribute inside an object or module by placing a dot between the object name and the attribute name.
Dot notation in math.sqrt(25) finds the sqrt function inside the already imported math module.

E

Edge case
An edge case is an unusual input at an extreme or special limit, such as an empty list, that can expose behavior ordinary inputs miss.
An edge case using a one-item list can expose a total function that starts its loop at the second item.
Exception
An exception signals a problem detected during execution; an unhandled exception stops the program and normally produces a traceback.
An exception from division by zero can stop average before its caller prints the calculated mean.

F

Floor
The floor of a number is the greatest integer no larger than that number; math.floor moves toward negative infinity when needed.
The floor of -3.2 is -4, so math.floor(-3.2) differs from conversion with int(-3.2).
Frame
A frame in a traceback identifies an active call location by its file, line number and function, with the relevant source line.
The lowest frame can show a division by an empty list's length, while earlier frames reveal which caller supplied that list.

H

Hypothesis
A hypothesis is a specific proposed explanation for a bug that predicts an observation you can check in the running program.
The hypothesis that a loop skips its first item predicts that a print of its indexes will never show 0.

I

ImportError
An ImportError can occur when from requests a name that is unavailable in a module, even though Python found the module itself.
An ImportError from from math import square_root points to the requested name, not an absent installation.
In-place operation
An in-place operation changes an existing object, such as a list, instead of requiring a new object to hold the changed contents.
The in-place operation random.shuffle(cards) changes cards and returns None, so it needs no assignment.
IndexError
An IndexError occurs when a sequence lookup asks for a position that does not exist, such as index 3 in a three-item list.
An IndexError from names[4] suggests checking the list's length and the loop bounds that produced 4.
Input validation
Input validation checks whether outside data meets a program's requirements and responds to unsuitable entries before using them.
Input validation with if score < 0 remains active when Python runs with assertions removed.
Interpreter
An interpreter is the program that runs Python code; different installed interpreters can have different third-party packages available.
Choosing the editor's interpreter explains why a package installed for another Python may still be missing.

K

KeyError
A KeyError occurs when a dictionary lookup requests an absent key, so Python cannot return a value for that key by indexing.
A KeyError for "price" can reveal that the current dictionary never received the expected price entry.

L

Logic error
A logic error makes a program behave incorrectly even though its statements execute without producing an exception report.
A logic error in a loop that starts at index 1 skips the first price and gives a total that is too small.

M

Main guard
A main guard is an if __name__ == "__main__": block that runs its body for direct execution and skips it during an ordinary import.
A main guard around main() lets tests import the module's functions without launching its application.
Minimal reproducible example
A minimal reproducible example is the smallest code and input that preserve a failure so the relevant steps can be examined directly.
A minimal reproducible example may reduce hundreds of records to the two that still trigger the same failure.
Module
A module is a unit of Python code, usually a .py file, whose functions and variables can be imported for use in another program.
The math module supplies sqrt, so a program can calculate square roots without defining that function.
Module search path
The module search path is the ordered list of folders in sys.path that Python checks after its built-in modules when finding imports.
A helper file outside the module search path may produce ModuleNotFoundError when the script imports it.
Module shadowing
Module shadowing happens when an earlier-found module has the name of the intended module and hides it during import lookup.
Module shadowing by a local random.py can leave a script without the library's randint function.

N

NameError
A NameError occurs when Python cannot find a binding for a name used in an expression, often because of a typo or missing import.
A NameError for sqrt after only import math means the function needs the math. prefix there.

O

Optimize option
The optimize option -O changes Python execution by removing assert statements, so required validation cannot rely on those statements.
Starting a script with the optimize option makes an assertion-only check of a negative age disappear.

P

Package
A package groups related Python modules, commonly within a folder, so programs can organize and reuse a larger collection of code.
A third-party package can supply several related modules that a program imports for different jobs.
pip
pip is an installation tool that downloads and manages third-party Python distributions for the interpreter that runs it.
Running python3 -m pip show cowsay asks pip associated with python3 to show that package's installation details.
Print debugging
Print debugging adds temporary output at selected code locations to reveal values, execution order and whether a hypothesis matches behavior.
Print debugging with print(f"{i=} {total=}") labels the loop index and total at the inspected moment.
Pseudo-random number
A pseudo-random number is produced by a deterministic algorithm whose sequence is designed to look random for uses such as simulations.
A pseudo-random number from random.randint(1, 6) can represent a die result in a board-game simulation.
PyPI
PyPI is the Python Package Index, a public repository where publishers make third-party distributions available for installation.
The PyPI page for a package provides project information and documentation to check before installing it.

R

Regression
A regression is a bug introduced into previously working behavior, often after an edit changes code that other functions or tests depend on.
A regression can make an earlier passing single-item total fail after a change intended to repair empty input.
Runtime error
A runtime error is a problem detected while code executes, such as an invalid operation that raises an exception at that moment.
A runtime error occurs when a valid division expression is reached with a divisor whose value is zero.

S

Sampling without replacement
Sampling without replacement chooses different input positions, so each position is selected at most once even if values repeat.
Sampling without replacement from ["A", "A", "B"] can select both positions that contain "A".
Seed
A seed initializes a pseudo-random generator, allowing the same call sequence to be repeated on the same Python version.
Setting the seed before a simulation lets a programmer inspect the same sequence again while debugging.
Specification
A specification states the behavior a function must provide, giving an independent basis for inputs, expected results and valid test checks.
The specification that scores of at least 90 earn an A sets the expected answer for a test at exactly 90.
Standard library
The standard library is the collection of modules included with Python, providing ready-made tools without a separate package installation.
The standard library includes math for calculations and random for simulations such as rolling dice.
Step into
Step into is a debugger action that enters a function called on the current line, so you can inspect the steps inside that function.
Use step into on result = convert(text) when the returned value is wrong and the input text looks correct.
Step over
Step over is a debugger action that executes the current line, including called functions, without following their internal lines.
Use step over on a calculation call when you want to inspect its returned result in the caller.
Syntax error
A syntax error means code breaks Python's grammar, such as a missing colon, and prevents the affected script from starting normally.
A syntax error after if ready can be fixed by supplying the colon required before its indented block.

T

Test discovery
Test discovery finds runnable tests using file and method naming rules, such as unittest's default test*.py file pattern.
Test discovery through python -m unittest can run matching files without listing each one in a command.
Test error
A test error is a test outcome caused by an unexpected exception other than a failed assertion, in the test or the code it calls.
A test error marked E can result when a function raises TypeError before the equality check receives a value.
Test failure
A test failure is a test outcome caused by a false assertion, such as an actual return value differing from the specified expected value.
A test failure marked F can show that the function returned "B" where the checked rule required "A".
Test method
A test method is a function inside a test class whose name starts with test, allowing unittest to discover and execute its checks.
A test method named test_limit can check a boundary separately from the test for an ordinary input.
TestCase
A TestCase is unittest's base class for test classes, supplying assertion methods and support for running each discovered test.
A class that inherits from unittest.TestCase can call self.assertEqual inside its named test methods.
Top-level code
Top-level code consists of module-level statements that execute in order when a module first loads, outside function bodies.
Top-level code that calls print can produce output during import even when no function is called by the importer.
Trace table
A trace table records variable values at successive execution steps, letting you follow a program by hand and inspect its changes.
A trace table for an accumulating loop shows the total before and after each item is added.
Traceback
A traceback reports the active chain of calls at an unhandled exception, ending with the exception type and its message.
Read the traceback's last line first, then follow the failing frame upward toward the original call.
TypeError
A TypeError signals an unsuitable type for an operation or an incorrect function-call argument count, such as a missing required argument.
A TypeError from "Score: " + 8 identifies a string and integer that cannot be joined by that expression.
Typosquatting
Typosquatting is publishing under a lookalike name to catch people who mistype a trusted project's name when installing software.
Checking a project's exact spelling on PyPI helps avoid typosquatting when copying an installation command.

U

Unit test
A unit test is a small automated check of a unit such as a function, comparing its behavior for chosen input with an expected outcome.
A unit test checks that a price function returns 0 for an empty list when its specification requires that result.

V

ValueError
A ValueError signals that an operation accepts the input's type but cannot use its particular value, such as invalid number text.
A ValueError from int("6.2") means the string does not describe a whole number in the required form.

W

Wildcard import
A wildcard import uses * to bring a module's public names into the current program, which can obscure origins and replace existing bindings.
The wildcard import from math import * can replace a variable called e with the mathematical constant.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started