Lab 5

De la WikiLabs
Jump to navigationJump to search

Lab 5 — Project Development I

Objectives

This laboratory marks the transition from language-oriented laboratories to supervised project development.

The main objective is to transform the semester project from an initial prototype into a structured, maintainable application with a clear architecture and a working minimum viable product (MVP).

Unlike the previous laboratories, most of the time in this session should be dedicated to project implementation, code review and technical discussion.

After completing this laboratory, you should be able to:

  • describe the architecture of your project;
  • separate responsibilities between modules;
  • identify unnecessary coupling between components;
  • define clear interfaces between parts of the application;
  • distinguish between domain logic, input/output and infrastructure code;
  • use composition and dependency injection where appropriate;
  • manage project dependencies;
  • maintain a clean Git history;
  • use branches and commits effectively;
  • handle configuration separately from application logic;
  • identify and remove global mutable state;
  • refactor large functions and classes;
  • improve testability;
  • document the main project structure;
  • demonstrate a working MVP.

The focus of this laboratory is not to introduce many new Python language features. Instead, the concepts from Labs 1–4 are applied to the semester project.

1. Laboratory structure

A recommended structure for the two-hour laboratory is:

Time Activity
0–15 min Project status and architecture review
15–35 min Short discussion: architecture, responsibilities and interfaces
35–95 min Supervised project implementation
95–110 min Code review and technical feedback
110–120 min MVP demonstration and next milestone

The exact timing may vary depending on the number and size of project teams.

2. The goal of the MVP

By the end of this laboratory, every project should have a working minimum viable product.

An MVP is not a complete final application. It should demonstrate that the central technical idea works.

Project type Possible MVP
image processing load an image, apply one meaningful processing operation and save/display the result
network application establish communication and exchange one meaningful message
data analysis load real data and produce one correct analysis result
hardware interface communicate with hardware and perform one useful operation
REST application expose one functional endpoint and process a real request
monitoring application collect one real metric and present or store it correctly

The MVP should prove the technical feasibility of the project.

3. Prototype versus project

A prototype is often written quickly to prove that something is possible.

A project must also be maintainable.

A prototype may look like:

main.py

containing:

  • configuration;
  • file handling;
  • calculations;
  • network communication;
  • user interaction;
  • error handling.

This may be acceptable for the first experiment, but it is not a suitable final structure.

4. Separating responsibilities

A more maintainable structure separates different responsibilities.

For example:

project/
├── src/
│   └── project_name/
│       ├── __init__.py
│       ├── models.py
│       ├── services.py
│       ├── storage.py
│       ├── config.py
│       └── main.py
└── tests/

A possible responsibility split is:

Module Responsibility
models.py application data structures
services.py application/domain logic
storage.py persistence and file/database access
config.py configuration loading and validation
main.py application startup and orchestration

The exact names depend on the project.

5. Single responsibility

A module, class or function should ideally have one clear reason to change.

Consider:

def process_students():
    # Read JSON file.
    # Parse students.
    # Validate grades.
    # Calculate statistics.
    # Print results.
    # Write CSV file.
    # Send HTTP request.
    ...

This function contains several unrelated responsibilities.

A better design might separate:

load_students()
validate_students()
calculate_statistics()
save_report()
send_report()

Each function is easier to understand and test.

6. Comparison with C++ project decomposition

The same software-engineering principle applies in C++ and Python.

C++ Python
src/
├── main.cpp
├── storage.cpp
├── services.cpp
└── models.cpp

include/
├── storage.hpp
├── services.hpp
└── models.hpp
src/
└── project/
    ├── main.py
    ├── storage.py
    ├── services.py
    └── models.py

Python usually does not separate declarations from implementations into header and source files.

7. High cohesion

Code inside one module should be strongly related.

Good:

storage.py

load_students()
save_students()
load_configuration()

Potentially poor:

utils.py

load_students()
resize_image()
send_email()
calculate_average()
parse_network_packet()

A generic utils.py file often becomes a collection of unrelated functions.

Prefer meaningful modules with clear responsibilities.

8. Low coupling

Modules should avoid unnecessary dependencies on each other.

A simple dependency direction such as:

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

is easier to reason about than a design where every module imports and depends on every other module.

9. Circular dependencies

A common architectural problem is:

module_a imports module_b
module_b imports module_a

For example:

models.py services.py
from services import calculate_score
from models import Student

This may create a circular import and usually indicates misplaced responsibilities.

10. Fixing circular dependencies

Possible solutions include:

  • move shared abstractions to a third module;
  • move logic to the module where it belongs;
  • invert the dependency;
  • pass objects or functions as parameters instead of importing them globally.

A simpler dependency structure is often:

models.py
    ↑
services.py
    ↑
main.py

rather than having models.py call into services.py.

11. Interfaces between components

Components should communicate through small, explicit interfaces.

Less clear:

storage.data
storage.filename
storage.format
storage.cache
storage.internal_state

Prefer a small public interface:

storage.load()
storage.save(data)

The implementation details remain inside the component.

12. C++ interface versus Python protocol

In C++, an interface may be represented using an abstract base class.

Python can use inheritance, but often only the required behavior matters.

C++ Python
class Storage {
public:
    virtual Data load() = 0;

    virtual void save(
        const Data& data
    ) = 0;

    virtual ~Storage() = default;
};
class JsonStorage:
    def load(self):
        ...

    def save(self, data):
        ...

Any Python object providing the expected methods can be used by code that relies on this behavior.

13. Dependency injection

A component should not always create its own dependencies internally.

Less flexible:

class StudentService:
    def __init__(self):
        self.storage = JsonStorage(
            "students.json"
        )

More flexible:

class StudentService:
    def __init__(self, storage):
        self.storage = storage

Application setup:

storage = JsonStorage("students.json")
service = StudentService(storage)

Tests can now use a fake storage implementation.

14. C++ dependency injection versus Python

C++ Python
class Service {
public:
    Service(Storage& storage)
        : storage(storage)
    {
    }

private:
    Storage& storage;
};
class Service:
    def __init__(self, storage):
        self.storage = storage

The architectural idea is the same.

15. Dependency inversion

High-level application logic should ideally depend on behavior rather than a specific implementation.

Instead of:

StudentService
      ↓
JsonStorage

think in terms of:

StudentService
      ↓
storage behavior
      ↑
JsonStorage
CsvStorage
MemoryStorage

This makes testing and future changes easier.

16. Avoid unnecessary abstractions

Do not create interfaces, factories and abstraction layers merely because they are possible.

For a project that will only ever read one small JSON file:

def load_students(path):
    ...

may be sufficient.

Use abstractions when they solve a real design problem.

17. Functions versus classes

Not every Python component needs to be a class.

Use a function when:

  • no persistent state is required;
  • the operation is naturally expressed as input → output;
  • no meaningful object identity exists.

Example:

Unnecessary class Simpler function
class AverageCalculator:
    def calculate(self, values):
        return sum(values) / len(values)
def average(values):
    return sum(values) / len(values)

A class is useful when state belongs together and several operations work on that state.

18. Large functions

A function that performs many distinct operations is difficult to:

  • understand;
  • test;
  • reuse;
  • modify safely.

Example:

def process_file(path):
    # Open file.
    # Parse format.
    # Validate records.
    # Calculate metrics.
    # Print report.
    # Save results.
    # Send network request.
    ...

Break large functions into smaller operations with clear names.

19. Refactoring a large function

Before:

def run(path):
    content = Path(path).read_text()

    data = json.loads(content)

    students = []

    for item in data:
        if not 1 <= item["grade"] <= 10:
            raise ValueError()

        students.append(
            Student(
                item["name"],
                item["grade"],
            )
        )

    avg = sum(
        student.grade
        for student in students
    ) / len(students)

    print(avg)

After:

def load_json(path):
    ...


def parse_students(data):
    ...


def average(students):
    ...


def main():
    data = load_json("students.json")
    students = parse_students(data)

    print(average(students))

The second version separates responsibilities and is easier to test.

20. Large classes

A class that handles many unrelated responsibilities may become a "god object".

Warning signs include:

  • many unrelated methods;
  • many attributes;
  • methods manipulating unrelated subsets of attributes;
  • knowledge of storage, user interface and domain logic simultaneously.

Example:

ApplicationManager
├── load_json()
├── save_csv()
├── calculate_average()
├── send_email()
├── draw_graph()
├── authenticate_user()
└── create_report()

This should probably be several components.

21. Data models

Simple data structures should remain simple.

For example:

from dataclasses import dataclass


@dataclass
class Student:
    name: str
    grade: float

Do not place unrelated application behavior inside a data model merely because the class already exists.

22. Domain logic

Domain logic represents the rules of the application.

Examples include:

  • calculating grades;
  • validating measurements;
  • processing images;
  • selecting routing decisions;
  • determining alarms;
  • filtering sensor data.

Domain logic should generally not depend directly on:

  • terminal input;
  • GUI widgets;
  • HTTP request objects;
  • database cursors.

This separation makes domain logic reusable and testable.

23. Input/output boundaries

Try to separate:

Input
   ↓
Parsing
   ↓
Domain logic
   ↓
Formatting
   ↓
Output

For example:

def calculate_average(students):
    ...


def main():
    students = load_students(...)

    result = calculate_average(students)

    print(result)

The calculation does not know where the students came from or where the result will be displayed.

24. Configuration

Do not scatter configuration values throughout the code.

Poor:

timeout = 5

...

requests.get(url, timeout=5)

...

if retry_count > 3:
    ...

Prefer centralized configuration:

from dataclasses import dataclass


@dataclass
class Config:
    timeout: float
    max_retries: int

or a configuration file.

25. Constants versus configuration

A constant is part of the program's logic.

Configuration is something that may reasonably differ between installations or runs.

Value More likely
mathematical constant constant
network server hostname configuration
API timeout configuration
protocol packet size fixed by specification constant
output directory configuration

26. Environment variables

Secrets and deployment-specific values should not be hard-coded.

Poor Better
API_KEY = "abc123-secret"
import os


api_key = os.environ["API_KEY"]

Do not commit passwords, tokens or private keys to Git.

27. Paths should not be hard-coded

Avoid:

path = "/home/alice/project/data/file.json"

Prefer:

from pathlib import Path


path = Path("data") / "file.json"

or accept the path as configuration.

28. Error boundaries

Not every function should catch every exception.

Low-level functions can often raise meaningful exceptions.

For example:

def load_students(path):
    return json.loads(
        path.read_text(
            encoding="utf-8"
        )
    )

Higher-level application code decides what the user should see:

def main():
    try:
        students = load_students(...)
    except FileNotFoundError:
        print("Student file not found")
        return

29. Do not suppress errors silently

Avoid:

try:
    process()
except Exception:
    pass

This hides failures and makes debugging difficult.

If an error is intentionally ignored, the reason should be explicit and narrow.

30. Logging project events

Use logging for diagnostics that may be useful during operation.

import logging


logger = logging.getLogger(__name__)


def load_students(path):
    logger.info(
        "Loading students from %s",
        path,
    )

Use print() for intentional user-facing terminal output and logging for diagnostics.

31. Project dependencies

Every third-party dependency should have a reason to exist.

Before adding a package, ask:

  • does the standard library already provide the functionality?
  • is the package significantly simplifying the implementation?
  • is it compatible with the project's Python version?
  • is it required by the application or only by development tooling?

32. Standard library versus external dependency

For a small CSV file:

Standard library External library
import csv
import pandas as pd

If dataframe functionality is not required, csv may be sufficient.

Avoid dependencies that add complexity without solving a real problem.

33. Dependency declaration

Dependencies should be recorded in project metadata.

Example:

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

dependencies = [
    "requests>=2.32",
]

Another developer should not have to guess which packages to install.

34. Reproducibility

Another student or evaluator should be able to clone the project and determine:

  • which Python version is expected;
  • which dependencies are required;
  • how to install them;
  • how to start the application;
  • how to run tests.

These instructions belong in the README.md.

35. Git is part of the project

The Git repository is not merely a place to submit the final files.

It should document the development history.

Useful commit messages include:

Add JSON storage layer
Implement student validation
Add average calculation tests
Refactor configuration loading
Handle missing input files

36. Poor commit messages

Avoid:

update
fix
stuff
changes
final
final2
final_final
asdf

Commit messages should indicate what changed.

37. Commit scope

A commit should ideally represent one coherent change.

Poor:

Implement API client,
rename every variable,
format whole project,
change storage,
add documentation

Better:

Add API client

Add retry handling

Refactor storage interface

Document API configuration

Small coherent commits are easier to review and revert.

38. Git workflow

A simple project workflow is:

main
  │
  ├── feature/json-storage
  │
  ├── feature/report-generation
  │
  └── fix/input-validation

For small student teams, a lightweight branch workflow is sufficient.

39. Creating a feature branch

git switch -c feature/json-storage

Work normally:

git add .
git commit -m "Add JSON storage layer"

Then merge after review.

40. Keep main usable

The main branch should ideally remain in a usable state.

Before merging:

ruff check .
mypy src
pytest

A branch that fails the project's normal checks should generally not be merged.

41. Merge conflicts

A merge conflict means Git cannot determine automatically how two changes should be combined.

Example:

<<<<<<< HEAD
timeout = 5
=======
timeout = 10
>>>>>>> feature/config

The developer must decide which final content is correct and remove the conflict markers.

42. Team ownership

Avoid assigning entire files permanently to individual team members.

For example:

Student A owns main.py
Student B owns services.py
Student C owns storage.py

This can produce isolated development where nobody understands the complete project.

Instead, assign features or responsibilities while maintaining shared understanding of the architecture.

43. Code review

Before accepting a significant change, another team member should inspect it.

A code review should consider:

  • correctness;
  • clarity;
  • architecture;
  • duplication;
  • error handling;
  • test coverage;
  • naming;
  • unnecessary complexity.

The purpose is to improve code, not to evaluate the programmer personally.

44. Questions for code review

Useful review questions include:

  • What responsibility does this function or class have?
  • Why is this a class rather than a function?
  • Why does this module import that module?
  • What happens with invalid input?
  • Can this dependency be replaced during testing?
  • Is this behavior tested?
  • Is this abstraction necessary?
  • Can this code be simplified?
  • Is this duplicated elsewhere?
  • Does the caller need to know this implementation detail?

45. Naming

Names should communicate intent.

Poor Better
def proc(x, d):
    ...
def calculate_average(
    students,
    minimum_grade,
):
    ...

Longer descriptive names are often preferable to short ambiguous names.

46. Python naming conventions

Common conventions include:

Element Convention Example
variable snake_case student_count
function snake_case load_students
class PascalCase StudentRepository
constant UPPER_CASE MAX_RETRIES
internal name leading underscore _parse_record

47. Comments

Comments should explain why, not merely repeat the code.

Poor:

# Increment i.
i += 1

Useful:

# Skip the first row because the
# device includes a proprietary header.
data = data[1:]

Readable code should explain most of the what by itself.

48. Docstrings

Public modules, classes and non-trivial functions may benefit from docstrings.

Example:

def calculate_average(
    grades: list[float],
) -> float:
    # Return the arithmetic mean of grades.
    if not grades:
        raise ValueError("grades is empty")

    return sum(grades) / len(grades)

In the actual project, a short function docstring can be used when additional explanation is useful.

Do not write verbose documentation for trivial code when the name and type hints already make the behavior obvious.

49. Type hints as interfaces

Type hints make module boundaries clearer.

Compare:

Less explicit More explicit
def process(data):
    ...
def process(
    students: list[Student],
) -> Report:
    ...

The second version communicates more about the expected interface.

50. Return values should be predictable

Avoid functions that sometimes return unrelated types.

Poor:

def load_data(path):
    if error:
        return False

    if empty:
        return None

    return records

Prefer a consistent return type and exceptions for failure:

def load_data(
    path: Path,
) -> list[Record]:
    ...

51. Avoid magic values

Poor:

if status == 3:
    ...

Better:

STATUS_READY = 3

if status == STATUS_READY:
    ...

Or use an enumeration when appropriate.

52. Enumerations

Python provides Enum.

C++ Python
enum class Status {
    Idle,
    Running,
    Error
};
from enum import Enum


class Status(Enum):
    IDLE = "idle"
    RUNNING = "running"
    ERROR = "error"

Use enums when a value belongs to a small, fixed set of meaningful states.

53. Avoid unnecessary global variables

Poor:

students = []
config = {}
connection = None
current_user = None

Global mutable state creates hidden dependencies.

Prefer explicit state:

class Application:
    def __init__(
        self,
        config,
        storage,
    ):
        self.config = config
        self.storage = storage

or pass data between functions.

54. Mutable default arguments

A common Python-specific bug is:

def add_student(
    student,
    students=[],
):
    students.append(student)

    return students

The same list is reused across calls.

Correct:

def add_student(
    student,
    students=None,
):
    if students is None:
        students = []

    students.append(student)

    return students

This issue should be checked during code review.

55. Resource management

Files and other resources should be managed explicitly.

Less robust Preferred
file = open(path)

data = file.read()

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

The context manager guarantees cleanup even if an exception occurs.

56. Avoid duplicated code

If the same non-trivial logic appears several times, consider extracting it.

Before:

if not 1 <= grade <= 10:
    raise ValueError()

...

if not 1 <= grade <= 10:
    raise ValueError()

After:

def validate_grade(grade):
    if not 1 <= grade <= 10:
        raise ValueError()

Do not remove every repeated line mechanically. Extract repeated concepts, not coincidental similarity.

57. Avoid premature optimization

Do not complicate the project for performance before measuring a real problem.

Correctness and clarity come first.

If performance is part of the project, measure it.

A simple timing experiment:

from time import perf_counter


start = perf_counter()

result = process(data)

elapsed = perf_counter() - start

print(f"{elapsed:.6f} s")

For serious benchmarking, use repeated measurements and dedicated tools.

58. Project startup

The project should have one obvious way to start.

For example:

python -m project_name

or:

python -m project_name.main

The README should document the expected command.

59. Application entry point

Keep startup code thin.

def main():
    config = load_config()
    storage = create_storage(config)

    app = Application(
        config,
        storage,
    )

    app.run()


if __name__ == "__main__":
    main()

The entry point assembles components rather than implementing the entire application.

60. Architecture should follow the problem

An image-processing project may need:

io.py
pipeline.py
filters.py
models.py
cli.py

A network application may need:

protocol.py
transport.py
messages.py
client.py
server.py

A data-analysis project may need:

loaders.py
preprocessing.py
analysis.py
visualization.py
main.py

Choose module boundaries based on responsibilities.

61. Example — image-processing pipeline

A poor design:

def main():
    image = cv2.imread(...)

    # preprocessing
    ...
    # filtering
    ...
    # segmentation
    ...
    # statistics
    ...
    # visualization
    ...
    # saving
    ...

A clearer design:

def load_image(path):
    ...


def preprocess(image):
    ...


def segment(image):
    ...


def calculate_metrics(image):
    ...


def save_result(path, result):
    ...

62. Example — networking project

Separate protocol logic from transport where possible.

protocol.py
    encode_message()
    decode_message()

transport.py
    send()
    receive()

client.py
    application workflow

This allows protocol parsing to be tested without opening a real network connection.

63. Example — hardware project

Separate hardware access from application logic.

device.py
    low-level communication

controller.py
    application behavior

models.py
    data structures

main.py
    startup

Tests can replace the device implementation with a fake device.

64. Project review checklist

During the laboratory, each project should be reviewed using the following questions:

Area Question
goal What problem does the project solve?
MVP What is the smallest useful end-to-end workflow?
architecture What are the main components?
modules Does each module have a clear responsibility?
dependencies Are dependencies explicit?
state Is unnecessary global mutable state present?
errors What failures are expected and how are they handled?
tests Can central logic be tested without external systems?
configuration Are machine-specific values separated from code?
Git Does the history contain meaningful commits?

65. Student architecture explanation

Each team should be able to explain its architecture in approximately two minutes.

A useful structure is:

Input
  ↓
Component A
  ↓
Component B
  ↓
Component C
  ↓
Output

The explanation should identify:

  • where data enters the system;
  • where central processing occurs;
  • where external dependencies exist;
  • where data leaves or is stored.

66. Module dependency diagram

Each team should create a simple dependency diagram.

Example:

main
 ├── config
 └── service
      ├── model
      └── storage

Avoid diagrams that include every individual function.

The objective is to understand high-level structure.

67. MVP demonstration

The MVP demonstration should be based on running software.

A useful demonstration includes:

  1. starting the application;
  2. providing real or representative input;
  3. executing the central workflow;
  4. showing the result;
  5. demonstrating one handled error condition.

Slides are not required for this milestone.

68. Failure demonstration

A robust project should also demonstrate expected failure behavior.

Examples include:

  • missing file;
  • invalid input;
  • unavailable device;
  • malformed message;
  • timeout;
  • unsupported format.

The program should fail predictably and explain the problem appropriately.

69. No silent fallback

Avoid behavior such as:

try:
    load_real_data()
except Exception:
    data = []

unless an empty dataset is genuinely the intended fallback.

Unexpected errors should remain visible.

70. Code review exercise

Consider:

def run():
    data = json.load(
        open("data.json")
    )

    x = []

    for d in data:
        if d["v"] > 5:
            x.append(d["v"] * 2)

    print(x)

Identify at least five issues or improvement opportunities.

Possible topics include:

  • resource management;
  • naming;
  • hard-coded path;
  • separation of I/O and logic;
  • type hints;
  • testability;
  • error handling.

71. Possible refactoring

One possible direction is:

import json
from pathlib import Path


def load_data(path: Path):
    with path.open(
        encoding="utf-8"
    ) as file:
        return json.load(file)


def transform_values(records):
    return [
        record["v"] * 2
        for record in records
        if record["v"] > 5
    ]


def main():
    records = load_data(
        Path("data.json")
    )

    values = transform_values(records)

    print(values)

This is not the only correct design.

72. Testing after refactoring

The transformation is now easy to test:

def test_transform_values():
    records = [
        {"v": 2},
        {"v": 7},
        {"v": 10},
    ]

    assert transform_values(records) == [
        14,
        20,
    ]

The architecture improves testability without adding unnecessary complexity.

73. Technical debt

Technical debt is code that works now but makes future changes more difficult.

Common examples in student projects include:

  • duplicated logic;
  • giant functions;
  • unexplained global variables;
  • hard-coded paths;
  • temporary debug code;
  • inconsistent naming;
  • missing error handling;
  • unnecessary dependencies;
  • tests that only work on one machine.

Lab 5 is the appropriate moment to remove obvious technical debt before more features are added.

74. Do not rewrite everything

Refactoring should be targeted.

A complete rewrite immediately before the final project phase is risky.

Prefer:

identify problem
      ↓
add/verify tests
      ↓
refactor small area
      ↓
run tests
      ↓
continue

Keep working behavior intact while improving the design.

75. MVP checkpoint requirements

By the end of Lab 5, the project should satisfy the following minimum requirements:

Requirement Expected state
repository project stored in Git
structure application split into meaningful modules
environment dependencies installable in a virtual environment
entry point one documented way to start the application
central feature working end-to-end
errors at least one expected failure handled
tests important domain logic covered by automated tests
configuration machine-specific values not scattered through code
README installation and execution instructions present
quality normal lint/test commands succeed

76. Exercise 1 — Architecture review

For your project, write down:

  1. the main input;
  2. the main output;
  3. the central processing step;
  4. external dependencies;
  5. persistent storage, if any;
  6. the main modules.

Draw a simple dependency diagram.

77. Exercise 2 — Responsibility audit

For every source module, write one sentence:

This module is responsible for ...

If the sentence contains several unrelated responsibilities joined by and, consider whether the module should be split.

78. Exercise 3 — Large function review

Find the largest function in the project.

Answer:

  • How many responsibilities does it have?
  • Which parts are pure computation?
  • Which parts perform I/O?
  • Which parts can become separate functions?
  • Can the central logic be tested independently?

Refactor it if appropriate.

79. Exercise 4 — Dependency review

List all third-party Python packages used by the project.

For each package, record:

Package Why it is needed Where it is used
example HTTP communication client.py

Remove unused dependencies.

80. Exercise 5 — Global state review

Search the project for mutable global variables.

For every one, determine whether it should instead be:

  • a local variable;
  • an object attribute;
  • configuration;
  • passed as a function parameter;
  • intentionally constant.

Refactor unnecessary global state.

81. Exercise 6 — Error handling review

Identify at least three realistic failure conditions for the project.

For each one, document:

Failure:
Detection:
Exception / return behavior:
User-visible behavior:
Log behavior:

Implement at least one missing error path.

82. Exercise 7 — Testability review

Choose one component that is difficult to test.

Identify why.

Possible causes include:

  • direct network access;
  • direct hardware access;
  • global state;
  • direct input();
  • hard-coded paths;
  • mixed I/O and computation.

Refactor the component so its central logic can be tested without the external dependency.

83. Exercise 8 — Git review

Inspect:

git log --oneline --graph --decorate

Check:

  • commit messages;
  • commit size;
  • accidental generated files;
  • secrets;
  • temporary files;
  • branch organization.

Correct repository hygiene problems.

84. Exercise 9 — README review

The README should contain at least:

Project name
Short description
Requirements
Installation
Running the application
Running the tests
Project structure
Current limitations

Another student should be able to start the project using only the README.

85. Exercise 10 — MVP demonstration

Demonstrate the current MVP to another student or instructor.

The demonstration must show:

  1. application startup;
  2. one representative input;
  3. central project functionality;
  4. output;
  5. one error condition.

Record any issue discovered during the demonstration and fix the highest-priority one.

86. Project milestone submission

At the end of Lab 5, the project repository should contain:

project/
├── README.md
├── pyproject.toml
├── .gitignore
├── src/
│   └── project_name/
│       └── ...
└── tests/
    └── ...

The repository should not contain:

.venv/
__pycache__/
*.pyc
passwords
API keys
large temporary files
IDE-specific build artifacts

unless a specific file is intentionally required.

87. Suggested instructor review

For each team, the instructor can perform a short review based on:

Area Review question
architecture Can the team explain the main components?
MVP Does the central workflow actually run?
Python Is the code written idiomatically?
coupling Are components unnecessarily dependent on each other?
testability Can important logic be tested independently?
robustness Are expected errors handled?
Git Is the development history meaningful?
documentation Can another developer run the project?

88. C/C++ habits to reconsider

C/C++ habit Typical Python project approach
create classes for every operation use functions for stateless behavior
create header/source pairs organize behavior into modules
hide every field behind getters expose simple state directly when appropriate
use globals for shared application state pass dependencies explicitly
create deep inheritance hierarchies prefer simple composition where possible
tightly couple code to concrete implementations inject replaceable dependencies when useful
depend on compiler diagnostics alone use tests, linting and type checking
large final integration maintain a continuously working MVP

89. Summary

The main goal of this laboratory is not to learn another Python syntax feature.

It is to improve the structure of the semester project.

A healthy project should increasingly have:

clear modules
     ↓
clear responsibilities
     ↓
explicit dependencies
     ↓
testable logic
     ↓
controlled configuration
     ↓
meaningful Git history
     ↓
working MVP

The central idea of this laboratory is:

A successful software project is not only a collection of working features. Its structure should make those features understandable, testable and safe to change.

90. Preparation for Lab 6

Before the next laboratory:

  1. ensure the MVP works end-to-end;
  2. resolve the highest-priority architecture issues identified during review;
  3. remove unnecessary global state;
  4. improve error handling;
  5. make central logic independently testable;
  6. ensure dependencies are documented;
  7. update the README;
  8. ensure tests and linting pass;
  9. create a short list of remaining features and known issues.

In Lab 6, the focus will be on completing the implementation, expanding tests, refactoring remaining technical debt, improving documentation and preparing a release candidate.