Lab 3

De la WikiLabs
Jump to navigationJump to search

Lab 3 — Modules, Packages, Files, Exceptions and Type Hints

Objectives

This laboratory moves from individual Python scripts and classes to the structure of complete Python applications.

Students already know how to split C/C++ applications into multiple source and header files. The objective of this laboratory is to understand the equivalent mechanisms in Python and to learn the conventions used for organizing real Python projects.

After completing this laboratory, you should be able to:

  • split a Python program into multiple modules;
  • create and use Python packages;
  • understand the role of __init__.py;
  • use absolute and relative imports;
  • understand the purpose of if __name__ == "__main__";
  • read and write text files;
  • use pathlib for filesystem paths;
  • read and write CSV and JSON files;
  • use exceptions for error handling;
  • define custom exceptions;
  • use try, except, else and finally;
  • use context managers and the with statement;
  • write type hints for variables, functions and collections;
  • use Optional, unions and type aliases;
  • understand the purpose of static type checkers;
  • organize a small Python project using a src/ and tests/ layout.

Throughout this laboratory, Python concepts are compared with familiar C/C++ mechanisms whenever a direct comparison is useful.

1. From translation units to modules

In C/C++, larger programs are usually split into header and source files.

In Python, a source file is also a module.

C/C++ Python
project/
├── main.cpp
├── math_utils.cpp
└── math_utils.hpp
project/
├── main.py
└── math_utils.py

A Python file can contain:

  • functions;
  • classes;
  • constants;
  • executable statements.

For example:

# math_utils.py

def add(a, b):
    return a + b


def multiply(a, b):
    return a * b

2. Importing a module

The Python import statement plays a role similar to including declarations and linking implementation code in C/C++.

C/C++ Python
#include "math_utils.hpp"

int result = add(2, 3);
import math_utils

result = math_utils.add(2, 3)

Using the module name explicitly makes the origin of the function clear.

3. Importing specific names

Python can import individual names from a module.

C/C++ Python
#include "math_utils.hpp"

int result = add(2, 3);
from math_utils import add

result = add(2, 3)

Both styles are common:

import math_utils

math_utils.add(2, 3)

or:

from math_utils import add

add(2, 3)

4. Import aliases

Modules or imported names can be given aliases.

C/C++ Python
namespace fs = std::filesystem;
import numpy as np
import pandas as pd

This style is common when a library has a well-known conventional alias.

Aliases should improve readability rather than obscure the origin of a name.

5. Avoid wildcard imports

Python allows:

from math_utils import *

However, this is generally discouraged.

It makes it difficult to determine where a name originated.

Compare:

Less explicit Preferred
from math_utils import *

result = add(2, 3)
from math_utils import add

result = add(2, 3)

or:

import math_utils

result = math_utils.add(2, 3)

6. Module execution

A Python module may be:

  • executed directly;
  • imported by another module.

This distinction is visible through the special variable __name__.

C/C++ Python
int main()
{
    run_program();
    return 0;
}
def main():
    run_program()


if __name__ == "__main__":
    main()

When a file is executed directly:

__name__ == "__main__"

When it is imported, __name__ contains the module name.

7. Why the main guard matters

Consider the following file:

# tools.py

print("Initializing tools")


def calculate():
    return 42

Importing it:

import tools

will immediately print:

Initializing tools

Top-level statements execute when a module is imported.

For code that should run only when the file is executed directly:

def main():
    print("Running application")


if __name__ == "__main__":
    main()

8. Packages

A package groups related modules into a directory.

A simple package might look like:

project/
├── main.py
└── calculator/
    ├── __init__.py
    ├── arithmetic.py
    └── statistics.py

The equivalent organizational idea in C++ might involve namespaces and multiple translation units.

C++ Python
calculator/
├── arithmetic.cpp
├── arithmetic.hpp
├── statistics.cpp
└── statistics.hpp
calculator/
├── __init__.py
├── arithmetic.py
└── statistics.py

9. __init__.py

Traditionally, __init__.py marks a directory as a Python package.

It can be empty:

# calculator/__init__.py

It can also define which names are conveniently exposed by the package.

For example:

# calculator/__init__.py

from .arithmetic import add
from .statistics import average

Then users can write:

from calculator import add, average

instead of:

from calculator.arithmetic import add
from calculator.statistics import average

10. Absolute imports

An absolute import starts from the top-level package.

Suppose the project is:

project/
└── app/
    ├── __init__.py
    ├── models.py
    └── services.py

Inside services.py:

from app.models import Student

This is an absolute import.

11. Relative imports

Modules inside a package may also use relative imports.

Inside services.py:

from .models import Student

A single dot means:

the current package

Two dots mean the parent package:

from ..utils import load_config

For student projects, absolute imports are often easier to read unless package-local relative imports clearly improve organization.

12. Namespaces

C++ namespaces and Python modules/packages solve related organizational problems.

C++ Python
namespace math_utils {

int add(int a, int b)
{
    return a + b;
}

}

int result =
    math_utils::add(2, 3);
# math_utils.py

def add(a, b):
    return a + b
import math_utils

result = math_utils.add(2, 3)

A module naturally provides a namespace.

13. A first multi-file Python program

Consider:

student_app/
├── main.py
├── models.py
└── services.py

models.py:

from dataclasses import dataclass


@dataclass
class Student:
    name: str
    grade: float

services.py:

from models import Student


def average(students: list[Student]) -> float:
    total = sum(student.grade for student in students)
    return total / len(students)

main.py:

from models import Student
from services import average


def main():
    students = [
        Student("Alice", 9.5),
        Student("Bob", 8.0),
    ]

    print(average(students))


if __name__ == "__main__":
    main()

14. Reading text files

C++ commonly uses std::ifstream.

Python uses the built-in open() function.

C++ Python
#include <fstream>
#include <string>

std::ifstream file("data.txt");

std::string line;

while (std::getline(file, line)) {
    std::cout << line << "\n";
}
with open("data.txt", "r") as file:
    for line in file:
        print(line, end="")

The Python with statement automatically closes the file.

15. Context managers

The with statement is used with objects that manage resources.

It is conceptually related to deterministic resource management in C++.

C++ RAII Python context manager
{
    std::ifstream file("data.txt");

    // Use file.

} // file is closed here
with open("data.txt") as file:
    # Use file.
    ...
# File is closed here.

The context manager guarantees cleanup even if an exception occurs inside the block.

16. Reading an entire file

C++ Python
std::ifstream file("data.txt");

std::string content(
    (std::istreambuf_iterator<char>(file)),
    std::istreambuf_iterator<char>()
);
with open("data.txt", "r") as file:
    content = file.read()

17. Reading lines

Python can read all lines into a list:

with open("data.txt", "r") as file:
    lines = file.readlines()

Usually, if the entire list is not required, direct iteration is preferable:

with open("data.txt", "r") as file:
    for line in file:
        ...

This avoids loading the entire file into memory.

18. Writing text files

C++ Python
std::ofstream file("output.txt");

file << "Hello\n";
file << "Value: " << value << "\n";
with open("output.txt", "w") as file:
    file.write("Hello\n")
    file.write(f"Value: {value}\n")

Common modes include:

Mode Meaning
"r" read
"w" write, replacing existing content
"a" append
"x" create, failing if the file already exists
"rb" binary read
"wb" binary write

19. Text encoding

For text files, specifying an encoding explicitly is recommended.

with open(
    "data.txt",
    "r",
    encoding="utf-8",
) as file:
    content = file.read()

This makes the program's assumptions explicit and avoids relying on platform-specific defaults.

20. pathlib

Modern Python code commonly uses pathlib.Path instead of manually manipulating path strings.

C++ Python
#include <filesystem>

namespace fs = std::filesystem;

fs::path path =
    fs::path("data") / "students.txt";
from pathlib import Path

path = Path("data") / "students.txt"

The / operator joins path components.

21. Common pathlib operations

Operation Python
test whether path exists path.exists()
test whether path is a file path.is_file()
test whether path is a directory path.is_dir()
get filename path.name
get extension path.suffix
get parent path.parent
create directory path.mkdir()

Example:

from pathlib import Path

data_dir = Path("data")

data_dir.mkdir(
    parents=True,
    exist_ok=True,
)

22. Reading and writing with pathlib

For small text files, Path provides convenience methods.

Traditional open() pathlib
with open(
    "data.txt",
    "r",
    encoding="utf-8",
) as file:
    content = file.read()
from pathlib import Path

path = Path("data.txt")

content = path.read_text(
    encoding="utf-8"
)

Writing:

path.write_text(
    "Hello\n",
    encoding="utf-8",
)

23. CSV files

CSV data should generally be processed using Python's csv module rather than manually splitting lines.

Suppose:

name,grade
Alice,9.5
Bob,8.0
Carol,7.5

Reading it:

import csv

with open(
    "students.csv",
    "r",
    encoding="utf-8",
    newline="",
) as file:
    reader = csv.reader(file)

    for row in reader:
        print(row)

24. CSV dictionaries

When the first row contains column names, csv.DictReader is often more readable.

import csv

with open(
    "students.csv",
    "r",
    encoding="utf-8",
    newline="",
) as file:
    reader = csv.DictReader(file)

    for row in reader:
        print(row["name"], row["grade"])

Each row behaves like a dictionary.

25. Writing CSV files

import csv

students = [
    ["Alice", 9.5],
    ["Bob", 8.0],
]

with open(
    "students.csv",
    "w",
    encoding="utf-8",
    newline="",
) as file:
    writer = csv.writer(file)

    writer.writerow(["name", "grade"])
    writer.writerows(students)

26. JSON files

JSON is commonly used for configuration files, APIs and structured data.

Example JSON:

{
    "name": "Alice",
    "year": 2,
    "active": true
}

Python's standard library provides the json module.

27. Reading JSON

Conceptual C++ approach Python
// Usually requires an external JSON library,
// for example nlohmann/json.
//
// std::ifstream file("config.json");
// json config = json::parse(file);
import json

with open(
    "config.json",
    "r",
    encoding="utf-8",
) as file:
    config = json.load(file)

The resulting Python object normally contains dictionaries, lists, strings, numbers, booleans and None.

28. Writing JSON

import json

config = {
    "host": "localhost",
    "port": 8000,
    "debug": True,
}

with open(
    "config.json",
    "w",
    encoding="utf-8",
) as file:
    json.dump(
        config,
        file,
        indent=4,
    )

29. Exceptions

Python exceptions serve the same general purpose as C++ exceptions.

C++ Python
try {
    int value = std::stoi(text);
}
catch (const std::invalid_argument& e) {
    std::cout << "Invalid input\n";
}
try:
    value = int(text)
except ValueError:
    print("Invalid input")

30. Raising exceptions

C++ uses throw.

Python uses raise.

C++ Python
if (value < 0) {
    throw std::invalid_argument(
        "value cannot be negative"
    );
}
if value < 0:
    raise ValueError(
        "value cannot be negative"
    )

31. Catching multiple exception types

Python can handle different exception types separately.

C++ Python
try {
    // ...
}
catch (const std::invalid_argument& e) {
    // ...
}
catch (const std::runtime_error& e) {
    // ...
}
try:
    ...
except ValueError:
    ...
except RuntimeError:
    ...

32. Accessing the exception object

C++ Python
catch (const std::exception& e) {
    std::cerr << e.what() << "\n";
}
except ValueError as error:
    print(error)

33. Avoid catching everything

This is possible:

try:
    ...
except Exception:
    ...

but catching broad exceptions without a clear reason can hide programming errors.

Prefer catching the specific errors that the code is expected to handle.

For example:

try:
    value = int(text)
except ValueError:
    print("Expected an integer")

34. try / except / else

Python provides an else block that executes only if no exception was raised.

try:
    value = int(text)
except ValueError:
    print("Invalid integer")
else:
    print(f"Parsed value: {value}")

This can keep the protected section of code small.

35. finally

A finally block executes whether or not an exception occurred.

C++ idea Python
try {
    // Work.
}
catch (...) {
    // Handle error.
}

// Cleanup must still happen,
// preferably through RAII.
try:
    ...
except ValueError:
    ...
finally:
    print("Always executed")

For resource cleanup, context managers are usually preferable to manually written finally blocks.

36. Custom exceptions

Application-specific errors can be represented by custom exception classes.

C++ Python
class InvalidGrade
    : public std::runtime_error
{
public:
    InvalidGrade()
        : std::runtime_error(
            "invalid grade"
        )
    {
    }
};
class InvalidGradeError(ValueError):
    pass

Usage:

if not 1 <= grade <= 10:
    raise InvalidGradeError(
        "grade must be between 1 and 10"
    )

37. Exception hierarchy

Python exceptions form a class hierarchy.

A simplified part of it is:

BaseException
└── Exception
    ├── ValueError
    ├── TypeError
    ├── OSError
    │   └── FileNotFoundError
    ├── KeyError
    ├── IndexError
    └── RuntimeError

Catching a base class also catches derived exception types.

Therefore:

except OSError:
    ...

also catches errors such as FileNotFoundError.

38. Common exception types

Exception Typical meaning
ValueError correct type, invalid value
TypeError operation used with inappropriate type
FileNotFoundError requested file does not exist
PermissionError insufficient filesystem permissions
KeyError dictionary key does not exist
IndexError sequence index outside valid range
ZeroDivisionError division by zero

39. EAFP versus LBYL

Python often follows the principle:

Easier to Ask Forgiveness than Permission (EAFP).

Instead of checking everything before an operation, perform the operation and handle the expected exception.

Compare:

Check first EAFP style
if "grade" in student:
    grade = student["grade"]
else:
    grade = None
try:
    grade = student["grade"]
except KeyError:
    grade = None

This does not mean exceptions should be used for every branch. Use the style that expresses the intent clearly.

40. Type hints

Python is dynamically typed, but it supports optional static type annotations.

C++ Python
int add(int a, int b)
{
    return a + b;
}
def add(a: int, b: int) -> int:
    return a + b

Python does not normally enforce these annotations at runtime.

They are primarily used by:

  • IDEs;
  • linters;
  • static type checkers;
  • documentation tools;
  • programmers reading the code.

41. Variable annotations

Variables can also be annotated.

C++ Python
int count = 0;
double average = 0.0;
std::string name = "Alice";
count: int = 0
average: float = 0.0
name: str = "Alice"

Annotations are optional when the type is obvious.

42. Collection type hints

Modern Python allows built-in collection types to be parameterized.

C++ Python
std::vector<int> values;

std::unordered_map<
    std::string,
    double
> grades;
values: list[int] = []

grades: dict[str, float] = {}

Other examples:

names: set[str] = set()

position: tuple[float, float] = (
    10.0,
    20.0,
)

43. Function annotations with collections

C++ Python
double average(
    const std::vector<double>& values
);
def average(
    values: list[float],
) -> float:
    return sum(values) / len(values)

44. Optional values

Sometimes a function may return either a value or no value.

In C++, std::optional can express this.

Modern Python can use the union operator |.

C++ Python
std::optional<Student> findStudent(
    const std::string& name
);
def find_student(
    name: str,
) -> Student | None:
    ...

Older Python code may use:

from typing import Optional


def find_student(
    name: str,
) -> Optional[Student]:
    ...

45. Union types

A value may have more than one accepted type.

C++ idea Python
std::variant<int, std::string> value;
value: int | str

Function example:

def parse_identifier(
    value: int | str,
) -> str:
    return str(value)

46. Type aliases

Complex type annotations can be given descriptive names.

C++ Python
using StudentGrades =
    std::unordered_map<
        std::string,
        std::vector<double>
    >;
StudentGrades = dict[
    str,
    list[float],
]

Then:

def average_grades(
    grades: StudentGrades,
) -> dict[str, float]:
    ...

47. The Any type

Any means that static type checking should accept any type.

from typing import Any


value: Any

Use it carefully.

Overusing Any removes much of the benefit of static type checking.

48. Static type checking

A static type checker can analyze annotations without executing the program.

For example, with:

def add(a: int, b: int) -> int:
    return a + b


result = add("hello", 3)

Python itself may only fail when that line executes.

A static checker can report the mismatch before execution.

A common checker is:

mypy .

Another modern option is:

pyright

49. Type hints are not runtime validation

This function:

def square(value: int) -> int:
    return value * value

can still be called at runtime with another compatible object:

print(square(3.5))

Type hints do not automatically reject the call.

If runtime validation is required, it must be implemented separately.

50. Project structure

For a larger project, avoid placing every file in one directory.

A useful structure is:

student-project/
├── pyproject.toml
├── README.md
├── .gitignore
├── src/
│   └── student_project/
│       ├── __init__.py
│       ├── models.py
│       ├── services.py
│       ├── storage.py
│       └── main.py
└── tests/
    ├── test_services.py
    └── test_storage.py

The src/ layout clearly separates application code from project metadata and tests.

51. C++ project layout versus Python project layout

Typical C/C++ Typical Python
project/
├── CMakeLists.txt
├── include/
│   └── project/
│       └── model.hpp
├── src/
│   ├── main.cpp
│   └── model.cpp
└── tests/
    └── test_model.cpp
project/
├── pyproject.toml
├── src/
│   └── project/
│       ├── __init__.py
│       ├── main.py
│       └── model.py
└── tests/
    └── test_model.py

52. pyproject.toml

Modern Python projects commonly use pyproject.toml for project metadata and tool configuration.

A minimal example:

[project]
name = "student-project"
version = "0.1.0"
requires-python = ">=3.11"

Dependencies can also be declared:

[project]
name = "student-project"
version = "0.1.0"
requires-python = ">=3.11"

dependencies = [
    "requests>=2.32",
]

53. Virtual environments

Each project should use its own virtual environment.

Create one:

python3 -m venv .venv

Activate it on Linux/macOS:

source .venv/bin/activate

Activate it on Windows PowerShell:

.venv\Scripts\Activate.ps1

Install dependencies:

python -m pip install requests

54. Why python -m pip?

Using:

python -m pip install requests

makes it explicit which Python interpreter is being used to run pip.

This is useful when multiple Python installations or virtual environments exist.

55. __pycache__ and .pyc files

Python may compile modules to bytecode and store them in:

__pycache__/

For example:

__pycache__/
└── models.cpython-313.pyc

These files should normally not be committed to Git.

A basic .gitignore includes:

.venv/
__pycache__/
*.pyc

56. Example project — Student grade manager

Consider the following project:

grade_manager/
├── pyproject.toml
├── src/
│   └── grade_manager/
│       ├── __init__.py
│       ├── models.py
│       ├── storage.py
│       ├── services.py
│       └── main.py
└── data/
    └── students.json

models.py:

from dataclasses import dataclass


@dataclass
class Student:
    name: str
    grade: float

57. Example project — storage module

storage.py:

import json
from pathlib import Path

from .models import Student


def load_students(
    path: Path,
) -> list[Student]:
    try:
        content = path.read_text(
            encoding="utf-8"
        )
    except FileNotFoundError:
        return []

    data = json.loads(content)

    return [
        Student(
            name=item["name"],
            grade=float(item["grade"]),
        )
        for item in data
    ]


def save_students(
    path: Path,
    students: list[Student],
) -> None:
    data = [
        {
            "name": student.name,
            "grade": student.grade,
        }
        for student in students
    ]

    path.write_text(
        json.dumps(data, indent=4),
        encoding="utf-8",
    )

58. Example project — service module

services.py:

from .models import Student


class EmptyStudentListError(ValueError):
    pass


def average(
    students: list[Student],
) -> float:
    if not students:
        raise EmptyStudentListError(
            "cannot calculate average "
            "for an empty list"
        )

    return sum(
        student.grade
        for student in students
    ) / len(students)

59. Example project — main module

main.py:

from pathlib import Path

from .services import (
    EmptyStudentListError,
    average,
)
from .storage import load_students


def main() -> None:
    path = Path("data/students.json")

    students = load_students(path)

    try:
        result = average(students)
    except EmptyStudentListError as error:
        print(error)
        return

    print(f"Average: {result:.2f}")


if __name__ == "__main__":
    main()

This example combines:

  • modules;
  • packages;
  • relative imports;
  • dataclasses;
  • file handling;
  • JSON;
  • exceptions;
  • type hints;
  • pathlib.

60. Common mistakes

Running package files directly

Suppose main.py contains:

from .services import average

Running:

python src/grade_manager/main.py

may fail because the file is being executed outside its package context.

Instead, from an appropriate project location, run the module:

python -m grade_manager.main

after installing the project or configuring the source path appropriately.

Circular imports

Avoid designs such as:

models.py imports services.py
services.py imports models.py

This usually indicates that responsibilities should be reorganized.

Too much code at module level

Prefer:

def main():
    ...


if __name__ == "__main__":
    main()

instead of placing the entire application directly at module scope.

61. C/C++ habits to reconsider

C/C++ habit Typical Python approach
separate declaration and implementation files usually keep a class or function definition in one .py module
include headers import modules or names
manually concatenate path strings use pathlib.Path
explicitly close files everywhere use with context managers
use error return codes for expected failures use exceptions when appropriate
create large monolithic source files split responsibilities into modules and packages
rely only on runtime type information use type hints and static analysis when useful
put all source files at project root use a clear project structure such as src/ and tests/

62. Exercise 1 — Split a program into modules

Start from this single-file program:

from dataclasses import dataclass


@dataclass
class Student:
    name: str
    grade: float


def average(students):
    return sum(
        student.grade
        for student in students
    ) / len(students)


students = [
    Student("Alice", 9.5),
    Student("Bob", 8.0),
]

print(average(students))

Split it into:

main.py
models.py
services.py

Use imports rather than duplicating code.

63. Exercise 2 — Build a package

Transform the previous exercise into:

student_app/
├── __init__.py
├── models.py
├── services.py
└── main.py

Requirements:

  • use package imports;
  • add a main() function;
  • use if __name__ == "__main__";
  • run the application as a module.

64. Exercise 3 — Text file processing

Create a program that reads:

grades.txt

with one grade per line.

For example:

9.5
8.0
7.5
10

The program must:

  1. read all valid grades;
  2. ignore empty lines;
  3. report invalid values;
  4. calculate the average;
  5. print the highest and lowest grade.

Use:

  • with;
  • exception handling;
  • type hints.

65. Exercise 4 — pathlib

Write a function:

def list_python_files(
    directory: Path,
) -> list[Path]:
    ...

that returns all .py files in a directory.

Use pathlib, not string concatenation.

Extension: search recursively.

66. Exercise 5 — CSV

Create a CSV file containing:

name,grade
Alice,9.5
Bob,8.0
Carol,7.0

Write functions:

def load_students(path: Path) -> list[Student]:
    ...


def save_students(
    path: Path,
    students: list[Student],
) -> None:
    ...

Use the csv module.

67. Exercise 6 — JSON configuration

Create:

{
    "minimum_grade": 5.0,
    "maximum_grade": 10.0,
    "output_file": "results.txt"
}

Write a Python program that:

  1. loads the configuration;
  2. validates the values;
  3. reports malformed or missing configuration;
  4. uses the configured output filename.

Use specific exception types where possible.

68. Exercise 7 — Custom exceptions

Create:

class InvalidGradeError(ValueError):
    pass

Then write:

def validate_grade(grade: float) -> None:
    ...

The function should raise InvalidGradeError when the grade is outside the accepted range.

Use it when constructing or loading students.

69. Exercise 8 — Type hints

Add complete type annotations to the following code:

def load_names(path):
    with open(path) as file:
        return [
            line.strip()
            for line in file
            if line.strip()
        ]


def find_name(names, target):
    for name in names:
        if name == target:
            return name

    return None

The final function should make it clear from its return type that the requested name may not exist.

70. Exercise 9 — Refactor a monolithic application

Consider an application that currently contains:

main.py

and mixes:

  • data classes;
  • file reading;
  • JSON parsing;
  • calculations;
  • user interaction.

Refactor it into at least:

models.py
storage.py
services.py
main.py

For every module, write one sentence describing its responsibility.

71. Exercise 10 — Mini-project structure

Create the following project:

grade_manager/
├── pyproject.toml
├── README.md
├── .gitignore
├── src/
│   └── grade_manager/
│       ├── __init__.py
│       ├── models.py
│       ├── storage.py
│       ├── services.py
│       └── main.py
└── data/
    └── students.json

Requirements:

  • Student must be a dataclass;
  • storage must use JSON;
  • paths must use pathlib;
  • invalid grades must raise a custom exception;
  • functions must use type hints;
  • the program must calculate the class average;
  • the main module must not contain storage implementation details.

72. Summary

The main mappings introduced in this laboratory are:

C/C++ Python
source/header organization modules and packages
#include import
namespace module/package namespace
main() if __name__ == "__main__"
std::ifstream open(..., "r")
std::ofstream open(..., "w")
RAII resource cleanup context manager / with
std::filesystem::path pathlib.Path
throw raise
catch except
custom exception class subclass of Exception or a more specific exception
static type declaration optional type annotations
std::optional<T> None
std::variant str
using alias type alias
build/project metadata pyproject.toml

The central idea of this laboratory is:

A maintainable Python application is not a single large script. It is organized into modules with clear responsibilities, explicit dependencies, robust error handling and well-defined interfaces.

73. Preparation for Lab 4

Before the next laboratory:

  1. complete the exercises from this laboratory;
  2. organize the semester project into multiple modules;
  3. create a virtual environment for the project;
  4. add a .gitignore;
  5. add basic type hints to public functions;
  6. separate data models, application logic and input/output code;
  7. ensure that expected errors are handled explicitly.

In Lab 4, the focus will move to testing, debugging and software quality using tools such as pytest, assertions, fixtures, logging and code-quality checks.