Quiz: Modules, Packages, and the Python Ecosystem

Test your understanding before moving on. Target: 70% or higher to proceed confidently.


Section 1: Multiple Choice (1 point each)

1. What is a Python module?

  • A) A directory containing multiple Python files
  • B) A single .py file that can be imported
  • C) A function that belongs to a class
  • D) A virtual environment for managing packages
Answer **B)** A single `.py` file that can be imported. *Why B:* A module is simply a `.py` file. Any Python file is a module that can be imported by other Python files. *Why not A:* That describes a package, not a module. *Why not C:* That describes a method, not a module. *Why not D:* Virtual environments are separate from the concept of modules. *Reference:* Section 12.1

2. After executing from math import sqrt, pi, which of the following will raise a NameError?

  • A) sqrt(16)
  • B) pi
  • C) math.ceil(4.2)
  • D) None of them; all three work
Answer **C)** `math.ceil(4.2)` *Why C:* `from math import sqrt, pi` imports only `sqrt` and `pi` into the namespace. The name `math` itself is not defined, so `math.ceil()` raises `NameError`. *Why not A:* `sqrt` was explicitly imported. *Why not B:* `pi` was explicitly imported. *Why not D:* A and B do work, but C does not, so "all three work" is false. *Reference:* Section 12.2

3. After import numpy as np, which line correctly calls NumPy's array() function?

  • A) numpy.array([1, 2, 3])
  • B) np.array([1, 2, 3])
  • C) array([1, 2, 3])
  • D) np.numpy.array([1, 2, 3])
Answer **B)** `np.array([1, 2, 3])` *Why B:* `import numpy as np` binds the module to the name `np`, so you reach everything in it through `np.`. (`np` is also the community convention, which is why you'll see it in nearly every NumPy example.) *Why not A:* The alias replaces the name. Only `np` is defined, so `numpy.array(...)` raises `NameError`. *Why not C:* An `import ... as` statement doesn't copy the module's contents into your namespace. You'd need `from numpy import array` for that. *Why not D:* `np` already *is* the numpy module. It has no attribute called `numpy`, so this raises `AttributeError`. *Reference:* Section 12.2

4. Why is from os import * considered bad practice?

  • A) It's a syntax error in Python 3.12
  • B) It imports well over a hundred names into your namespace, making it hard to track where names come from
  • C) It only works on Linux, not Windows
  • D) It makes your program slower because it loads the entire os module
Answer **B)** It imports well over a hundred names into your namespace, making it hard to track where names come from. *Why B:* `import *` dumps all public names from the module into your namespace. You can't tell where functions came from, and two `import *` statements can silently overwrite each other's names. `from os import *` even replaces the built-in `open()` with `os.open()`, a lower-level function that works differently. *Why not A:* It's valid syntax — it works, it's just bad practice. *Why not C:* It works on all platforms. *Why not D:* Python loads the entire module regardless of import style; the performance difference is negligible. *Reference:* Section 12.2

5. How many times does Python execute a module's code when it's imported multiple times in the same program?

  • A) Once per import statement
  • B) Exactly once, on the first import
  • C) It depends on whether you use import or from...import
  • D) Zero — Python only loads functions, not executable code
Answer **B)** Exactly once, on the first import. *Why B:* Python caches imported modules in `sys.modules`. The first `import` creates the module object and executes the file. Subsequent imports reuse the cached object. *Why not A:* Re-importing does not re-execute the module. *Why not C:* Both styles trigger loading once and only once. *Why not D:* Python does execute top-level code (assignments, print statements, etc.) on the first import. *Reference:* Section 12.2

6. What does collections.Counter do?

  • A) Counts the number of elements in a list
  • B) Creates a dict subclass that maps each element to its count
  • C) Counts from one number to another, like range()
  • D) Tracks how many times a function has been called
Answer **B)** Creates a dict subclass that maps each element to its count. *Why B:* `Counter` takes an iterable and produces a dictionary-like object where keys are elements and values are their counts. It also provides convenience methods like `.most_common()`. *Why not A:* That's just `len()`. *Why not C:* That's `range()` or `itertools.count()`. *Why not D:* That would be a decorator or closure pattern, not `Counter`. *Reference:* Section 12.3

7. What is the purpose of the file __init__.py in a directory?

  • A) It initializes Python variables when the program starts
  • B) It tells Python that the directory should be treated as a package
  • C) It must contain the main() function for the package
  • D) It lists all files that belong to the package
Answer **B)** It tells Python that the directory should be treated as a package. *Why B:* `__init__.py` marks the directory as a regular package, and Python runs it when the package is first imported. It can be empty or contain initialization code. (Since Python 3.3 a folder without it can still be imported as a *namespace package*, but that's a special-purpose feature. For your own packages, always include `__init__.py`.) *Why not A:* It doesn't initialize Python — it identifies a package. *Why not C:* It doesn't need to contain `main()` — any code or no code is valid. *Why not D:* Python discovers files automatically; `__init__.py` doesn't list them. *Reference:* Section 12.6

8. When you run python grading.py, what is the value of __name__ inside grading.py?

  • A) "grading"
  • B) "__main__"
  • C) "grading.py"
  • D) None
Answer **B)** `"__main__"` *Why B:* When a file is run directly (as the entry point), Python sets its `__name__` to `"__main__"`. This is why the guard `if __name__ == "__main__":` works. *Why not A:* `"grading"` would be the value if the file were imported, not run directly. *Why not C:* The `.py` extension is not included. *Why not D:* `__name__` is always a string, never `None`. *Reference:* Section 12.5

9. You have a file called random.py in your project directory. What happens when another file in the same directory, main.py, is run and executes import random?

  • A) Python imports the standard library random module
  • B) Python imports your random.py file instead of the standard library module
  • C) Python raises an ImportError because of the name conflict
  • D) Python imports both and merges them
Answer **B)** Python imports your `random.py` file instead of the standard library module. *Why B:* The script's directory is first in `sys.path`, so Python finds your `random.py` before the standard library's `random`. This is called "name shadowing." *Why not A:* The standard library directories are searched after the script's directory. *Why not C:* Python doesn't refuse the import. It just uses the first match. You only find out later, when something like `random.randint` turns out to be missing (Python 3.13+ then adds a "consider renaming" hint to the error). *Why not D:* Python never merges modules. *Reference:* Section 12.9

10. What command installs the requests package from PyPI?

  • A) python install requests
  • B) pip install requests
  • C) import requests --install
  • D) pip download requests
Answer **B)** `pip install requests` *Why B:* `pip install` is the standard command for installing packages from PyPI. *Why not A:* `python install` is not a valid command. *Why not C:* Import statements don't have command-line flags. *Why not D:* `pip download` downloads without installing. *Reference:* Section 12.7

Section 2: True/False with Justification (1 point each)

11. "Every Python file you create is automatically a module."

Answer **True** *Explanation:* Any `.py` file is a module. You can import it by name (without the `.py` extension) as long as Python can find it on the module search path. You don't need to add any special declaration to make a file importable. *Reference:* Section 12.1

12. "The __name__ guard prevents a module from being imported."

Answer **False** *Explanation:* The `__name__` guard does not prevent importing. It controls which code runs *when* the file is imported versus run directly. The functions and classes in the file are still importable. Only the code inside the `if __name__ == "__main__":` block is skipped on import. *Reference:* Section 12.5

13. "You can install standard library modules using pip install."

Answer **False** *Explanation:* Standard library modules (like `math`, `datetime`, `random`, `os`) ship with Python and are always available. You don't need to install them. `pip install` is for third-party packages from PyPI that are not included with Python. *Reference:* Sections 12.3, 12.7

14. "If two modules each import the other, Python will always raise an ImportError."

Answer **False** *Explanation:* Circular imports don't always fail — the outcome depends on the specific code structure and what names each module tries to use from the other at import time. However, they frequently cause `ImportError` or `AttributeError` and are considered a code smell. Restructuring to remove the cycle is the best practice. *Reference:* Section 12.9

Section 3: Short Answer (2 points each)

15. Complete the following table. Fill in the "Access Pattern" and "Potential Risk" for each import style.

Import Statement Access Pattern Potential Risk
import random ? ?
from random import randint ? ?
from random import * ? ?
Answer | Import Statement | Access Pattern | Potential Risk | |---|---|---| | `import random` | `random.randint(1, 10)` | Verbose, but no risk of name collision | | `from random import randint` | `randint(1, 10)` | Could shadow a local `randint` name | | `from random import *` | `randint(1, 10)` | Imports ALL names; can silently overwrite local names; unclear where names come from | *Rubric — full credit requires:* - Correct access pattern for each style - Identifying the risk level escalation from safe to dangerous *Reference:* Section 12.2

16. Explain what sys.path is and why the order of directories in it matters. Describe one specific scenario where the order causes a bug.

Answer `sys.path` is a list of directory paths that Python searches, in order, to find modules when you use `import`. The order matters because Python uses the first match it finds and stops searching. **Bug scenario:** If you create a file called `random.py` in your project directory, Python finds it before the standard library's `random` module (because the project directory is typically first in `sys.path`). Any attempt to use `random.randint()` or other standard library `random` functions will fail with an `AttributeError` because Python loaded your file instead. *Rubric — full credit requires:* - Defining `sys.path` as the search path list - Explaining first-match-wins behavior - Providing a concrete shadowing example *Reference:* Section 12.8

17. You are building a project with this structure:

inventory/
    __init__.py
    products.py
    orders.py
    reports.py

Write the import statements you would use in reports.py to import the Product class from products.py and the get_pending_orders() function from orders.py. Use relative imports.

Answer
from .products import Product
from .orders import get_pending_orders
The `.` indicates a relative import from the same package. The absolute form (`from inventory.products import Product`) works just as well; PEP 8 recommends absolute imports as the default and calls relative imports an acceptable alternative, which is why the question asks for them explicitly. *Rubric — full credit requires:* - Using relative import syntax (`.` prefix) - Correct module and name references *Reference:* Section 12.6

Section 4: Applied Scenario (3 points each)

18. Elena has a project with this structure:

reports/
    main.py
    data_loader.py
    analyzer.py
    formatter.py
    datetime.py       <-- !!!

She runs main.py and gets this error (on Python 3.12; newer versions append a hint, which we've hidden here):

AttributeError: module 'datetime' has no attribute 'datetime'

a) What caused this error? b) How should she fix it? c) What general rule does this illustrate?

Answer a) Elena has a file called `datetime.py` in her project directory. When any file in the project does `import datetime`, Python finds her `datetime.py` first (because the script's directory is searched before the standard library). Her file doesn't have a `datetime` class, causing the `AttributeError`. b) Rename `datetime.py` to something that doesn't clash — e.g., `date_utils.py` or `time_helpers.py` — and update any of her own imports that referred to it. A leftover `__pycache__/datetime.cpython-*.pyc` file can be deleted for tidiness, but it's harmless: Python ignores a cached file in `__pycache__` once its `.py` source is gone. c) Never name your files after standard library modules. Python's module search path checks the local directory first, so local files shadow standard library modules of the same name. *Rubric:* | Criterion | 0 pts | 1 pt | 2 pts | 3 pts | |-----------|-------|------|-------|-------| | Diagnosis | Not attempted | Vaguely identifies naming issue | Correctly identifies name shadowing | Full explanation with sys.path reasoning | | Fix | Not attempted | Incomplete fix | Correct rename | Rename + updated imports + general rule |

19. Dr. Patel has this DNA analysis code in one large file (dna_analysis.py, 600 lines). Propose a modular structure: list the modules you'd create, what functions each would contain, and write the main.py that imports and uses them. The code currently contains: - Functions for reading FASTA files - Functions for counting nucleotides and finding patterns - Functions for displaying results in tables - Functions for exporting results to CSV

Answer **Proposed structure:**
dna_project/
    main.py
    file_io.py        # read_fasta(), export_to_csv()
    analysis.py       # count_nucleotides(), find_pattern(), gc_content()
    display.py        # print_summary_table(), print_pattern_matches()
**`main.py`:**
import file_io
import analysis
import display

def main():
    sequences = file_io.read_fasta("sequences.fasta")

    for name, seq in sequences.items():
        counts = analysis.count_nucleotides(seq)
        gc = analysis.gc_content(seq)
        display.print_summary_table(name, counts, gc)

    pattern = "ATCG"
    matches = analysis.find_pattern(sequences, pattern)
    display.print_pattern_matches(pattern, matches)

    file_io.export_to_csv(sequences, "results.csv")

if __name__ == "__main__":
    main()
*Rubric — full credit requires:* - At least 3 modules with clear, non-overlapping responsibilities - Logical grouping of functions into modules - A `main.py` that imports and orchestrates - `__name__` guard on `main.py`

20. Write a complete, working program that uses at least three different standard library modules to create a "daily briefing" script. The script should: - Display the current date and time (formatted nicely) - Generate a random motivational quote from a list - Show the current working directory and count of Python files in it

Answer
from datetime import datetime
import random
from pathlib import Path

def daily_briefing():
    # Date and time
    now = datetime.now()
    print("=" * 40)
    print("  Daily Briefing")
    print(f"  {now.strftime('%A, %B %d, %Y')}")
    print(f"  {now.strftime('%I:%M %p')}")
    print("=" * 40)

    # Random quote
    quotes = [
        "The best time to plant a tree was 20 years ago. The second best time is now.",
        "Code is like humor. When you have to explain it, it's bad.",
        "First, solve the problem. Then, write the code.",
        "Simplicity is the soul of efficiency.",
        "Every expert was once a beginner.",
    ]
    print("\n  Quote of the day:")
    print(f"  \"{random.choice(quotes)}\"")

    # Directory info
    cwd = Path(".")
    py_files = list(cwd.glob("*.py"))
    print(f"\n  Working directory: {cwd.resolve()}")
    print(f"  Python files here: {len(py_files)}")
    for f in py_files:
        print(f"    - {f.name}")

if __name__ == "__main__":
    daily_briefing()
*Rubric — full credit requires:* - Uses at least 3 standard library modules - Code runs without errors on Python 3.13+ - Output is formatted and readable - Includes `__name__` guard

Scoring & Next Steps

Score Assessment Recommended Action
< 50% Needs review Re-read sections 12.1-12.5, redo Part A exercises
50-70% Partial Review weak areas, redo Part B exercises
70-85% Solid Ready to proceed; revisit any missed topics
> 85% Strong Proceed; consider the extension exercises in Part E