Introduction
This is my eighth article as a Java engineer learning TypeScript and Python from scratch.
My first seven articles were all small TypeScript CLIs. In the previous one, I built an HTTP client in TypeScript that calls a FastAPI (Python) server I wrote myself — that was my first contact with Python (FastAPI), but only as "the thing the client calls."
In other words, Python played a supporting role last time. From here, I start Python properly from "project 1." For this first one I go back to the basics: a weight tracker CLI.
What I focused on this time:
- Python basics (
#comments,def, snake_case, f-strings) -
Type hints (
list[float],tuple[float, float, float]) and how they differ from Java Generics - Input validation with
try / except ValueError - Extracting the calculation logic as a "pure function" (separation of concerns, testability)
- Unit testing with pytest (normal, boundary, and error cases)
- The habits an ex-Java engineer slips into — and correcting them
As always, I write honestly about where I got stuck, what I thought through, and what I asked AI for.
📝 Where this sits in the series
This is the first article in my Python series. It's also a chance to check whether the mindset I picked up in the TypeScript series — "think in types," "separate logic from I/O," "write tests" — still works when the language changes.
My Learning Style (AI Transparency)
💡 Learning companions & how this article is written
I use Claude Pro (design discussions and Q&A) and Cursor Pro (coding support) as learning companions. Beyond the code, I also collaborate with AI on writing this article itself. Stating the facts plainly here.
Division of roles:
- Tech selection, design, implementation, and code verification → me
- Article structure, outline, draft prose, and translation → in collaboration with Claude
- All content is checked and revised by me before publishing → me
My guiding principle is "the thinking is mine, the wording is AI-assisted, and I verify all of it." I write the code myself (I never ask AI to write code for me), I use AI for hints, spec clarification, and bug spotting, and I make sure I understand why before moving on — and I apply that same stance to the writing process.
In this article, I continue to clearly separate "what I implemented myself" from "what I asked AI for."
What I Built
Enter a menu number, and the CLI records a weight, lists recorded weights, or computes statistics (average, max, min).
--------------------------------
1. Record weight
2. Display the recorded weights
3. Display the weight calculation result
4. Exit
--------------------------------
Enter your choice: 1
Enter your weight(kg): 70.5
--------------------------------
Enter your choice: 3
Weight calculation result:
Average weight: 70.5
Max weight: 70.5
Min weight: 70.5
Data is held in memory and resets when you exit (a deliberate simplification for local learning). I'm saving persistence for the next stage.
📦 Repository: https://github.com/uya0526-design/weight_tracker_py
File Structure and Tech Stack
The structure is very simple.
weight_tracker_py/
├── tests/
│ ├── __init__.py
│ └── test_main.py # unit tests
├── main.py # entry point
└── requirements.txt # dependencies
Tech stack:
- Python 3.12
- pytest 9.0.3
The flow from creating a virtual environment (venv) to running tests:
python -m venv venv # project-local isolated environment
.\venv\Scripts\Activate.ps1 # activate (Windows PowerShell)
pip install pytest # install the test tool
pip freeze > requirements.txt # record dependencies
python main.py # run the app
pytest # run the tests
I understood venv as "per-project isolation," close to a Maven local repository or Node's node_modules.
What I Implemented Myself
main.py — The menu loop and function split
I run the menu with while True + if / elif, dispatching to a function per choice, and break on 4 to exit.
def main() -> None:
while True:
choice = display_main_menu()
if choice == "1":
record_weight()
elif choice == "2":
display_recorded_weights()
elif choice == "3":
display_weight_calculation_result()
elif choice == "4":
break
else:
print("Invalid choice")
if __name__ == "__main__":
main()
I understood if __name__ == "__main__": as a declaration of "this is the program's entry point," close to Java's public static void main(String[] args). It doesn't run when imported — only when the file is run directly.
Input validation — try / except ValueError
The weight is read as a string, so I check whether it can be converted to a number.
def record_weight() -> None:
weight = input("Enter your weight(kg): ")
# Check if the weight is a number
try:
weight = float(weight)
except ValueError:
print("Invalid weight")
return
recorded_weights.append(weight)
At first I tried to judge "is this a number?" with isdigit() and got stuck (more below). Structurally this is almost the same as Java's try-catch: a string that can't be converted by float() raises ValueError (close to Java's NumberFormatException).
Listing — enumerate and an empty-list check
I used enumerate to list items with numbers.
def display_recorded_weights() -> None:
print("Recorded weights: ")
if len(recorded_weights) == 0:
print("No weights recorded")
return
for index, weight in enumerate(recorded_weights):
print(f"{index + 1}. {weight}")
enumerate is a loop that gives you the index and the element at once, letting me write what I used to write as for (int i = 0; i < list.size(); i++) in Java much more cleanly. I wanted the display to start at 1, so I print index + 1.
Statistics — extracting a pure function (my biggest lesson this time)
This was the design highlight of the project. At first I bundled display and calculation into one function, but I extracted just the calculation logic as a "pure function that takes arguments and only returns values."
# display side (reads the global list → hard to test)
def display_weight_calculation_result() -> None:
print("Weight calculation result: ")
if len(recorded_weights) == 0:
print("No weights recorded")
return
average_weight, max_weight, min_weight = calculate_weight_calculation_result(recorded_weights)
print(f"Average weight: {average_weight}")
print(f"Max weight: {max_weight}")
print(f"Min weight: {min_weight}")
# calculation side (takes arguments, only returns values → easy to test)
def calculate_weight_calculation_result(weights: list[float]) -> tuple[float, float, float]:
average_weight = round(sum(weights) / len(weights), 2)
max_weight = max(weights)
min_weight = min(weights)
return average_weight, max_weight, min_weight
Two points here.
1. Don't read the global variable directly — take it as an argument
The display_... side reads the module-level recorded_weights. That depends on the result of input(), which makes it hard to test. The calculate_... side takes weights as an argument, so a test can pass any list it likes. This is separation of concerns — the same idea I kept coming back to in the TypeScript series, and it worked just as well in Python.
2. Return multiple values as a tuple
return average_weight, max_weight, min_weight returns three values at once. The caller can unpack them with a, b, c = .... In Java, "multiple return values" meant a dedicated class or an array, so writing it with plain syntax felt fresh.
The return type hint -> tuple[float, float, float] feels close to Generics like List<Float> in Java. The big difference is that it's not enforced at runtime (returning a value that doesn't match the type isn't an error), so I understood it as information for the reader and the IDE.
tests/test_main.py — normal, boundary, and error cases
Because I'd extracted a pure function, the tests were very straightforward to write.
import pytest
from main import calculate_weight_calculation_result
# normal: multiple items
def test_calculate_weight_calculation_result():
weights = [70.12345, 75, 80, 85, 90]
average_weight, max_weight, min_weight = calculate_weight_calculation_result(weights)
assert average_weight == 80.02
assert max_weight == 90.0
assert min_weight == 70.12345
# error: empty list → division by zero
def test_calculate_weight_calculation_result_empty_list():
with pytest.raises(ZeroDivisionError):
calculate_weight_calculation_result([])
# boundary: a single item
def test_calculate_weight_calculation_result_one_weight():
weights = [70.12345]
average_weight, max_weight, min_weight = calculate_weight_calculation_result(weights)
assert average_weight == 70.12
assert max_weight == 70.12345
assert min_weight == 70.12345
pytest.raises(ZeroDivisionError) corresponds to Java's assertThrows(ArithmeticException.class, ...). It's interesting that "this should throw an exception" can be written as a test too.
What I Asked AI For
| Topic | What AI helped with |
|---|---|
| Spec clarification | Surfacing unclear points: input validation, display format, how to exit, menu design |
| Comment syntax | Pointing out that Python uses #, not //
|
| Validation | Pointing out that isdigit() can't handle decimals |
| Conversion handling | Pointing out I was appending the raw string, and that double-converting with float() was unnecessary |
| Design suggestion | Suggesting I pass the global variable as an argument, for testability |
| Type hints | Pointing out a mismatch between -> None and -> list[float]
|
| Testing | The pytest.raises syntax, and the ideal folder structure |
| Dependencies | Explaining requirements.txt (direct deps only vs. pinning everything) |
| README | Tidying it up in both Japanese and English |
Note: I found and fixed the wrong test expectation (below) myself.
Where I Got Stuck
1. I wrote comments with //
Situation: I wrote a comment with // and it wouldn't run.
Root cause: A Java / TypeScript habit. Python comments use #.
Fix: Changed them all to #.
Takeaway: When the language changes, the first differences show up in the tiniest syntax. A small thing, but it reliably reminds me "you're writing a different language now."
2. isdigit() can't reject decimals
Situation: I used "70.5".isdigit() for input validation, and a valid weight got rejected.
Root cause: isdigit() returns True only when every character is a digit. A decimal point . makes it False, so a value like 70.5 doesn't pass.
Fix: I dropped the character-type check and switched to trying a float() conversion, catching failures with try / except ValueError.
# NG: a decimal point makes it False → rejects a valid weight
if weight.isdigit():
...
# OK: try to convert, reject on failure
try:
weight = float(weight)
except ValueError:
print("Invalid weight")
return
Takeaway: If you want to know "can this be used as a number?", actually trying to convert it is more reliable than inspecting character types. I learned to pick the check that fits the goal.
3. ZeroDivisionError on an empty list
Situation: Computing the average with zero recorded weights crashed the program.
Root cause: In sum(weights) / len(weights), len became 0, causing a division by zero (ZeroDivisionError).
Fix and design decision: I added an early return if len(recorded_weights) == 0 on the display side, returning "No weights recorded" to the user. On the other hand, I deliberately left the calculation function (calculate_...) to raise ZeroDivisionError as-is, and verified that with a test.
Takeaway: "Where to prevent an error" and "where to let an exception through" are separate decisions. I block early near the UI, while letting the pure function raise naturally and pinning that behavior with a test.
4. Can't import main.py from tests/
Situation: from main import ... in tests/test_main.py raised ModuleNotFoundError.
Root cause: tests/ wasn't recognized as a package, so the import couldn't be resolved.
Fix: Adding tests/__init__.py (an empty file) resolved it.
Takeaway: Folder structure is part of "the spec for making things run." Tracking down the cause myself was a small confidence boost.
5. My test expectation was wrong
Situation: A test failed. The assert for average_weight didn't match.
Root cause: It wasn't the implementation — my own expected value was wrong. I'd written the round() result from my head without actually computing it.
Fix: The average of [70.12345, 75, 80, 85, 90] is 400.12345 / 5 = 80.02469, and round(..., 2) makes it 80.02. I recomputed it by hand and corrected the expectation.
Takeaway: "A failing test" doesn't always mean "the implementation is wrong." Catching a wrong expectation is also part of a test's value. Experiencing the real-world cycle in miniature — a test fails, then you separate whether the cause is the implementation or the expectation — was my biggest gain this time.
What I Learned
Python basics (mapped to Java)
| Topic | Key takeaway |
|---|---|
| Comments | Use # (// is Java / JavaScript) |
| Function definition |
def name(): (corresponds to Java's static void) |
| Naming | snake_case record_weight (vs. Java's camelCase) |
| Multiple return values |
return a, b, c returns a tuple (Java needs a dedicated class or array) |
| String formatting | f-strings (f"{variable}") |
Type hints
- You can annotate arguments and return values (
list[float],tuple[float, float, float]) - Feels close to Java Generics (
List<Float>) - But it's not enforced at runtime — it's information for readability and IDE support
Exception handling
-
try / excepthas the same structure as Java'stry-catch - A string that
float()can't convert raisesValueError(close to Java'sNumberFormatException)
Testability (separation of concerns)
- Functions containing
input()orprint()are hard to test - Extracting the calculation logic as a pure function that takes arguments and returns values makes it testable
- This is exactly the "separation of concerns" that matters in real work
Unit testing (pytest)
| Topic | Key takeaway |
|---|---|
| Position |
pytest corresponds to Java's JUnit
|
| Exception tests |
with pytest.raises(...) corresponds to assertThrows(...)
|
| Coverage | Covering normal, boundary, and error cases is the baseline |
| Value | When a test fails, it can also reveal a wrong expectation |
Project structure
- Putting
__init__.pyintests/resolves pytest's import -
requirements.txtcan list "direct deps only" or "pin everything." When reproducibility matters, list everything
Reflection
I built this the same way as before: write the code myself and have AI review it. As my first Python project, three things stood out.
The habit of thinking in types carried straight over: The "think in types first" I repeated in the TypeScript series came naturally with Python's type hints. There's the difference that they're not enforced at runtime, but understanding the goal — "write them for the reader and the IDE" — was a real gain.
Separation of concerns transcends the language: Extracting the calculation logic as a pure function made the tests surprisingly easy to write. "Separate I/O from logic" works the same way in TypeScript and in Python.
The experience of a test failing is itself a lesson: Recomputing and fixing my own expectation taught me that "a test isn't just a tool for doubting the implementation." Separating whether a failure comes from the implementation or the expectation was the most satisfying part.
The ex-Java habits (// comments, isdigit()) did surface, but I corrected them along with the reasons, so I should be able to leave them behind for the next Python project.
Wrapping Up
This was my record of building a weight tracker CLI in Python, centered on type hints, pure functions, and pytest — my first article in the Python series.
Continuous progress from the TypeScript series:
-
Picked up Python basics while staying aware of the differences from Java (
#,def, snake_case, tuple multiple returns) - Wrote type hints while understanding that they're not enforced at runtime
- Extracted the calculation logic as a pure function, practicing separation of concerns in Python too
- Covered normal, boundary, and error cases with pytest
- Found and fixed a wrong test expectation myself, experiencing the same cycle as real work
Next, I want to move into persistence with file saving and JSON, and graduate from this project's "data disappears when you exit" simplification.
The full learning log is in the repository:
This article is part of my public learning journey using AI tools (Claude Pro / Cursor Pro). The thinking and all code are mine; I collaborate with AI on the writing (structure, drafting, translation) and verify every line before publishing.
Top comments (0)