Lab 5
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:
- starting the application;
- providing real or representative input;
- executing the central workflow;
- showing the result;
- 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:
- the main input;
- the main output;
- the central processing step;
- external dependencies;
- persistent storage, if any;
- 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:
- application startup;
- one representative input;
- central project functionality;
- output;
- 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:
- ensure the MVP works end-to-end;
- resolve the highest-priority architecture issues identified during review;
- remove unnecessary global state;
- improve error handling;
- make central logic independently testable;
- ensure dependencies are documented;
- update the README;
- ensure tests and linting pass;
- 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.