Programming with PythonPython Setup and First Programs › Day 49

Hands-on lab — Day 49: Your First Real Program

Commands

Setup

cd labs/sections/programming-with-python/day-049-your-first-real-program
python3 --version

Run

python3 examples/converter.py 100 C
python3 examples/converter.py 32 F
python3 starter/converter.py 212 F

Test

bash tests/run_tests.sh

File tree

examples/converter.py
expected-output/FIELDS.md
expected-output/sample-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/converter.py
starter/program-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 049 lab — Build a Complete Small Program

Lesson

  • Lesson title: Your First Real Program
  • Day number: 49 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-049-your-first-real-program
  • Lab files: everything you need is in this directory — follow “How to run” below.
  • Browse the course locally: from the repository root, this lab also appears in the course website at /labs/day-049-your-first-real-program when the site is running.

Purpose

Day 49's lesson assembles a week of Python pieces into one whole. This lab makes that concrete: you build a temperature converter — a genuinely complete, small, real program that reads input from the command line, validates it, does one useful job, prints clear output, and fails gracefully on bad input. You build it from a starter, one exercise at a time, then run an automated test suite that checks real behaviour. This is the shape the Week 7 project (the Command-Line Calculator) is built on.

Learning objectives

  • Read and understand a complete, well-structured Python program.
  • Write small named functions with docstrings and a main() that ties them together.
  • Add the if __name__ == "__main__": guard and explain why it makes the program both runnable and importable (hence testable).
  • Validate input at the boundary and report bad input with a clear message and a non-zero exit code.
  • Test a program without a human: run it on known inputs and import a function to check its return value.

Prerequisites

  • The Day 49 lesson (read it first — it explains every part this lab builds).
  • Days 43-48: a working Python 3 install plus variables, strings, numbers, input/output, and reading error messages.
  • A text editor and a terminal. No programming experience beyond this week is assumed.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14).
  • Linux — fully supported (any distribution with Python 3 and bash).
  • Windows — use WSL and follow the Linux path, or substitute python for python3 if that is how Python is exposed. The program itself is pure standard-library Python and behaves identically everywhere.

Hardware requirements

Any computer that runs Python 3. The program does only arithmetic on two numbers; it needs no special memory, disk, or GPU.

Required software

  • python3 (3.8 or newer; tested on 3.14).
  • bash for the test runner (preinstalled on macOS and Linux).
  • Standard library only — the sys module ships with Python. No packages to install. See requirements/README.md.

Free and open-source options

Everything here is free and open source: Python, bash, and the standard library. No account, API key, network access, or purchase is needed.

Installation

None beyond Python itself. Move into this directory and you are ready:

cd labs/sections/programming-with-python/day-049-your-first-real-program
python3 --version   # confirm Python 3.8+ is available

File structure

day-049-your-first-real-program/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── converter.py                ← YOUR working file (5 numbered exercises)
│   └── program-worksheet.md        ← design the program before coding it
├── examples/
│   └── converter.py                ← complete reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (good + bad inputs, imports)
├── expected-output/
│   ├── sample-run.txt              ← real captured run of the reference
│   ├── test-run.txt                ← real captured run of the test suite
│   └── FIELDS.md                   ← required behaviour on every platform
├── requirements/
│   └── README.md                   ← dependency statement (Python 3 only)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished program first, on good and bad input
python3 examples/converter.py 100 C
python3 examples/converter.py 32 F
python3 examples/converter.py hot C          # prints an error, exits non-zero

## 2. Your task: complete the five exercises in the starter, then run it
python3 starter/converter.py 212 F

## 3. Prove the module is importable (the payoff of the main guard)
python3 -c "import sys; sys.path.insert(0, 'examples'); from converter import celsius_to_fahrenheit; print(celsius_to_fahrenheit(100))"

## 4. Check your work
bash tests/run_tests.sh

What the commands do

  • python3 examples/converter.py 100 C — runs the complete reference program: it reads the value and unit from the command line, validates them in parse_args, converts in convert, formats with format_result, and prints 100.0 C = 212.0 F. Bad input (hot C, 100 K, a missing argument, a temperature below absolute zero) prints a clear error to standard error and exits with code 1.
  • python3 starter/converter.py 212 F — runs your version. The starter ships with five exercises stubbed out (each raising NotImplementedError until you finish it): write a conversion function, add the main guard, validate input, format output, and handle an edge case.
  • python3 -c "...celsius_to_fahrenheit..." — imports one function from the module and calls it, without running the whole program. This works only because the main guard holds main back on import.
  • bash tests/run_tests.sh — runs the reference on seven good and bad inputs (checking output and exit code), imports two functions to check their return values, and checks your starter (structurally until you finish, strictly afterwards). Exits 0 only if every check passes.

Expected output

See expected-output/sample-run.txt — a real captured run:

$ python3 examples/converter.py 100 C
100.0 C = 212.0 F

$ python3 examples/converter.py hot C   ; echo "exit: $?"
error: 'hot' is not a number
usage: python3 converter.py <value> <unit>   (unit is C or F)
exit: 1

The conversion prints to standard output; the error prints to standard error and sets exit code 1. The program is deterministic, so your numbers will match exactly. expected-output/FIELDS.md lists the required behaviour for every input on every platform.

Validation steps

  1. Run python3 examples/converter.py 100 C — it must print 100.0 C = 212.0 F.
  2. Run python3 examples/converter.py hot C; echo $? — it must print an error and then 1.
  3. Complete the five exercises in starter/converter.py, then run it on the same inputs and confirm it matches the reference.
  4. Run the tests (next section) — every check must pass.

Tests

bash tests/run_tests.sh

Expected final line while the starter is unfinished: 12 checks, 0 failure(s). Once you complete all five starter exercises, six more checks run your version through the same good/bad inputs plus the main-guard check, giving 18 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. A full captured run is in expected-output/test-run.txt.

Cleanup

Nothing to clean up: the program and tests read only their command-line arguments and write nothing outside their own console output (no files, no network, no settings). To reset your work, restore the starter from git: git checkout -- starter/converter.py.

Troubleshooting

See troubleshooting.md for the full list: python vs python3, the deliberate NotImplementedError stubs, importing vs running, keeping tests non-interactive, and exit-code checks.

Security notes

See security.md. Short version: the program makes no network calls, writes no files, and needs no privileges. Its central lesson is to validate input and never eval() it — turn text into numbers with float(), which cannot execute code.

Extension exercises

  1. Write your own tests/test_converter.py that imports the conversion functions and asserts several known results, printing all tests passed only if every assertion holds; run it with python3 tests/test_converter.py.
  2. Make the program self-documenting: running it with no arguments or --help should print the usage line and docstring and exit 0.
  3. Run the program as a module with python3 -m converter 100 C (from the folder containing the file) and confirm it behaves identically to running it by path.
  • Previous day: Day 48 — Reading Error Messages and Debugging (labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging/).
  • Next day: Day 50 — begins Week 8, Control Flow and Collections (labs/sections/programming-with-python/day-050-.../, to be written).
  • Week 7 project: the Command-Line Calculator, which extends exactly this program shape — parse input, validate, compute, print clearly, fail gracefully.

Expected output

FIELDS.md

# Expected output — Day 049 lab

This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, Python 3.14, 2026-07-12). Your numbers will match
exactly, because the converter is deterministic — the same input always
produces the same output on every platform.

## Files

- `sample-run.txt` — the reference program run on several good and bad
  inputs, plus the `python3 -c` import check.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
  still unfinished (12 checks, 0 failures).

## Required behaviour on every platform

A correct converter must, for these inputs, produce exactly:

| Command | Standard output | Exit code |
| ------- | --------------- | --------- |
| `converter.py 100 C` | `100.0 C = 212.0 F` | 0 |
| `converter.py 32 F` | `32.0 F = 0.0 C` | 0 |
| `converter.py 98.6 F` | `98.6 F = 37.0 C` | 0 |
| `converter.py hot C` | (stderr) `error: 'hot' is not a number` | 1 |
| `converter.py 100 K` | (stderr) `error: unit must be C or F, not 'K'` | 1 |
| `converter.py 100` | (stderr) `error: expected 2 arguments: ...` | 1 |
| `converter.py -300 C` | (stderr) `error: -300.0 C is below absolute zero` | 1 |

The only platform difference is the shell prompt shown before each command
(`$` here); the program's own output is identical everywhere Python 3 runs.

## Test-suite counts

- With the starter unfinished: `12 checks, 0 failure(s).`
- Once you complete all five starter exercises: `18 checks, 0 failure(s).`
  (the six extra checks run your starter through the same good/bad inputs as
  the reference, plus a check that it has the main guard).

sample-run.txt

$ python3 examples/converter.py 100 C
100.0 C = 212.0 F

$ python3 examples/converter.py 32 F
32.0 F = 0.0 C

$ python3 examples/converter.py 98.6 F
98.6 F = 37.0 C

$ python3 examples/converter.py hot C   ; echo "exit: $?"
error: 'hot' is not a number
usage: python3 converter.py <value> <unit>   (unit is C or F)
exit: 1

$ python3 examples/converter.py 100 K   ; echo "exit: $?"
error: unit must be C or F, not 'K'
usage: python3 converter.py <value> <unit>   (unit is C or F)
exit: 1

$ python3 examples/converter.py 100      ; echo "exit: $?"
error: expected 2 arguments: <value> <unit> (e.g. 100 C)
usage: python3 converter.py <value> <unit>   (unit is C or F)
exit: 1

$ python3 -c "import sys; sys.path.insert(0, 'examples'); from converter import celsius_to_fahrenheit; print(celsius_to_fahrenheit(100))"
212.0

test-run.txt

Testing <repo>/labs/sections/programming-with-python/day-049-your-first-real-program/examples/converter.py ...
  ok: 100 C -> 212.0 F
  ok: 32 F -> 0.0 C
  ok: 98.6 F -> 37.0 C
  ok: non-number rejected
  ok: bad unit rejected
  ok: wrong arg count
  ok: below absolute zero
Testing importability of examples/converter.py ...
  ok: import celsius_to_fahrenheit(100) == 212.0
  ok: import fahrenheit_to_celsius(32) == 0.0
Testing starter/converter.py ...
  ok: starter is valid Python
Note: starter/converter.py still has unfinished exercises — testing structure only.
  ok: starter defines celsius_to_fahrenheit
  ok: starter defines main

12 checks, 0 failure(s).

Source files

examples/converter.py (2755 bytes)
#!/usr/bin/env python3
"""Convert a temperature between Celsius and Fahrenheit.

This is a complete, small, real program: it reads input from the command
line, validates it, does one useful job, prints clear output, and fails
gracefully (a readable message and a non-zero exit code) on bad input.

Usage:
    python3 converter.py <value> <unit>

<value> is a number; <unit> is the unit you are converting FROM: C or F.

Examples:
    python3 converter.py 100 C   ->  100.0 C = 212.0 F
    python3 converter.py 32 F    ->  32.0 F = 0.0 C
"""
import sys

# Coldest physically possible temperature, used as a sanity-check edge case.
ABSOLUTE_ZERO_C = -273.15


def celsius_to_fahrenheit(celsius):
    """Return the Fahrenheit equivalent of a Celsius temperature."""
    return celsius * 9 / 5 + 32


def fahrenheit_to_celsius(fahrenheit):
    """Return the Celsius equivalent of a Fahrenheit temperature."""
    return (fahrenheit - 32) * 5 / 9


def parse_args(args):
    """Validate raw [value, unit] arguments and return (number, unit).

    Raises ValueError with a human-readable message on any bad input:
    wrong argument count, a non-numeric value, an unknown unit, or a
    temperature below absolute zero.
    """
    if len(args) != 2:
        raise ValueError("expected 2 arguments: <value> <unit> (e.g. 100 C)")
    value_text, unit_text = args
    try:
        value = float(value_text)
    except ValueError:
        raise ValueError(f"'{value_text}' is not a number")
    unit = unit_text.strip().upper()
    if unit not in ("C", "F"):
        raise ValueError(f"unit must be C or F, not '{unit_text}'")
    limit = ABSOLUTE_ZERO_C if unit == "C" else celsius_to_fahrenheit(ABSOLUTE_ZERO_C)
    if value < limit:
        raise ValueError(f"{value} {unit} is below absolute zero")
    return value, unit


def convert(value, unit):
    """Convert value from unit to the other unit; return (result, result_unit)."""
    if unit == "C":
        return celsius_to_fahrenheit(value), "F"
    return fahrenheit_to_celsius(value), "C"


def format_result(value, unit, result, result_unit):
    """Return the one-line, human-readable result string."""
    return f"{value:.1f} {unit} = {result:.1f} {result_unit}"


def main(argv):
    """Program entry point. Returns an exit code: 0 on success, 1 on bad input."""
    try:
        value, unit = parse_args(argv[1:])
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print("usage: python3 converter.py <value> <unit>   (unit is C or F)",
              file=sys.stderr)
        return 1
    result, result_unit = convert(value, unit)
    print(format_result(value, unit, result, result_unit))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
metadata.yml (656 bytes)
lesson_id: D049
day: 49
kind: python-program
languages: [python]
setup_commands:
  - cd labs/sections/programming-with-python/day-049-your-first-real-program
  - python3 --version
run_commands:
  - python3 examples/converter.py 100 C
  - python3 examples/converter.py 32 F
  - python3 starter/converter.py 212 F
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/converter.py  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 12 checks, 0 failure(s), exit 0'
requirements/README.md (787 bytes)
# Dependencies — Day 049 lab

**Python 3 only. No third-party packages.**

- `python3` (3.8 or newer; tested on 3.14). Preinstalled on most Linux
  distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — specifically the `sys` module,
  which ships with Python. There is deliberately no `requirements.txt`: a
  first real program should run on a plain Python install with nothing to
  install first.

Check your Python is present and new enough:

```bash
python3 --version
```

If that prints `Python 3.8` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system.
starter/converter.py (3168 bytes)
#!/usr/bin/env python3
"""Temperature converter — YOUR working file.

Build this program one exercise at a time. Each numbered exercise below
names exactly what to write. The finished reference is in
examples/converter.py — try each exercise yourself before peeking.

When you have completed all five exercises, this file should behave just
like the reference:

    python3 starter/converter.py 100 C   ->  100.0 C = 212.0 F
    python3 starter/converter.py hot C   ->  error (exit code 1)

Then run:  bash tests/run_tests.sh
"""
import sys

ABSOLUTE_ZERO_C = -273.15


def celsius_to_fahrenheit(celsius):
    """Return the Fahrenheit equivalent of a Celsius temperature."""
    # Exercise 1: WRITE A FUNCTION.
    # Return the Fahrenheit value: multiply by 9, divide by 5, add 32.
    # Verify by hand: celsius_to_fahrenheit(100) must be 212.0.
    raise NotImplementedError("Exercise 1: implement celsius_to_fahrenheit")


def fahrenheit_to_celsius(fahrenheit):
    """Return the Celsius equivalent of a Fahrenheit temperature."""
    # (Provided so the program is complete once Exercise 1 is done.)
    return (fahrenheit - 32) * 5 / 9


def parse_args(args):
    """Validate raw [value, unit] arguments and return (number, unit)."""
    # Exercise 3: VALIDATE INPUT.
    # 1. If len(args) != 2, raise ValueError with a clear message.
    # 2. Convert args[0] to float inside try/except; on failure raise
    #    ValueError(f"'{args[0]}' is not a number").
    # 3. Normalize args[1] with .strip().upper(); if it is not "C" or "F",
    #    raise ValueError naming the allowed units.
    # Exercise 5: HANDLE AN EDGE CASE.
    # 4. Reject temperatures below absolute zero (use ABSOLUTE_ZERO_C for C,
    #    and celsius_to_fahrenheit(ABSOLUTE_ZERO_C) for F).
    # Return (value, unit).
    raise NotImplementedError("Exercises 3 & 5: implement parse_args")


def convert(value, unit):
    """Convert value from unit to the other unit; return (result, result_unit)."""
    # (Provided.)
    if unit == "C":
        return celsius_to_fahrenheit(value), "F"
    return fahrenheit_to_celsius(value), "C"


def format_result(value, unit, result, result_unit):
    """Return the one-line, human-readable result string."""
    # Exercise 4: FORMAT OUTPUT.
    # Return an f-string like "100.0 C = 212.0 F", showing each number to
    # one decimal place (use the :.1f format specifier).
    raise NotImplementedError("Exercise 4: implement format_result")


def main(argv):
    """Program entry point. Returns an exit code: 0 on success, 1 on bad input."""
    try:
        value, unit = parse_args(argv[1:])
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print("usage: python3 converter.py <value> <unit>   (unit is C or F)",
              file=sys.stderr)
        return 1
    result, result_unit = convert(value, unit)
    print(format_result(value, unit, result, result_unit))
    return 0


# Exercise 2: ADD THE MAIN GUARD.
# Below this comment, add the guard so the program runs only when this file
# is executed directly (not when it is imported):
#
#     if __name__ == "__main__":
#         sys.exit(main(sys.argv))
starter/program-worksheet.md (1628 bytes)
# Program design worksheet — Day 049

Design your program *before* you code it. Fill this in for the temperature
converter first (to practise), then reuse a blank copy for the practice
assignment's second program. Keep this file — the Command-Line Calculator
week project builds on exactly this design habit.

## 1. What one useful job does the program do?

One sentence:

> _e.g. Convert a temperature between Celsius and Fahrenheit._

## 2. Inputs — what does it need, and from where?

| Input | Type | Source |
| ----- | ---- | ------ |
|       |      |        |
|       |      |        |

## 3. Processing — what does it do with the inputs?

List the steps in order (include validation):

1.
2.
3.

## 4. Outputs — what does it produce, and where?

- On success:
- On failure (bad input):
- Exit code on success ______  on failure ______

## 5. Edge cases — at least three

What weird or wrong input might arrive, and how should the program respond?

| Edge case | What the program should do |
| --------- | -------------------------- |
| e.g. value is not a number |  |
| e.g. missing an argument   |  |
| e.g. impossible value      |  |

## 6. Behaviour on real inputs (fill in after building)

- One GOOD input you ran: `___________________`
  - What it printed: `___________________`
  - Exit code: ____
- One BAD input you ran: `___________________`
  - What it printed (to stderr): `___________________`
  - Exit code: ____

## 7. Prove it is importable (main-guard payoff)

Paste the `python3 -c` one-liner you used to import one function and check
its return value, and the output you saw:

```text

```
tests/run_tests.sh (4107 bytes)
#!/usr/bin/env bash
# Tests for the Day 049 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Exercises the complete reference program (examples/converter.py) on known
# good and bad inputs, checking both the printed output and the process exit
# code, then imports a function from the module and checks its return value.
# Finally it checks the learner's starter: structurally while exercises are
# unfinished, and to the same strict standard once they are complete.
# No network, non-interactive. Exits 0 only if every check passes.
set -u

# Keep the working tree clean: do not let imported modules write __pycache__.
export PYTHONDONTWRITEBYTECODE=1

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/converter.py"
starter="${lab_dir}/starter/converter.py"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

# check_run <label> <script> <expect_exit> <needle> <arg...>
# Runs the program, checks its exit code equals expect_exit and that its
# combined output contains needle.
check_run() {
  local label="$1" script="$2" expect_exit="$3" needle="$4"
  shift 4
  local out code
  out="$(python3 "${script}" "$@" 2>&1)"
  code=$?
  if [ "${code}" -eq "${expect_exit}" ] && printf '%s' "${out}" | grep -qF "${needle}"; then
    check "${label}" "yes"
  else
    check "${label}" "no"
    echo "    (exit ${code}, expected ${expect_exit}; output: ${out})"
  fi
}

run_program_checks() {
  local script="$1"
  echo "Testing ${script} ..."
  # Good inputs: correct conversion, exit 0.
  check_run "100 C -> 212.0 F"      "${script}" 0 "100.0 C = 212.0 F" 100 C
  check_run "32 F -> 0.0 C"         "${script}" 0 "32.0 F = 0.0 C"    32 F
  check_run "98.6 F -> 37.0 C"      "${script}" 0 "98.6 F = 37.0 C"   98.6 F
  # Bad inputs: clear error, non-zero exit.
  check_run "non-number rejected"   "${script}" 1 "is not a number"   hot C
  check_run "bad unit rejected"     "${script}" 1 "unit must be C or F" 100 K
  check_run "wrong arg count"       "${script}" 1 "expected 2 arguments" 100
  check_run "below absolute zero"   "${script}" 1 "below absolute zero" -300 C
}

# --- Reference program: always tested strictly ---
run_program_checks "${ref}"

# --- Import a function and check its return value (main-guard payoff) ---
echo "Testing importability of examples/converter.py ..."
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from converter import celsius_to_fahrenheit; \
assert celsius_to_fahrenheit(100) == 212.0"; then
  check "import celsius_to_fahrenheit(100) == 212.0" "yes"
else
  check "import celsius_to_fahrenheit(100) == 212.0" "no"
fi
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from converter import fahrenheit_to_celsius; \
assert fahrenheit_to_celsius(32) == 0.0"; then
  check "import fahrenheit_to_celsius(32) == 0.0" "yes"
else
  check "import fahrenheit_to_celsius(32) == 0.0" "no"
fi

# --- Learner starter ---
echo "Testing starter/converter.py ..."
if python3 -c "compile(open('${starter}').read(), '${starter}', 'exec')" 2>/dev/null; then
  check "starter is valid Python" "yes"
else
  check "starter is valid Python" "no"
fi

if grep -q 'NotImplementedError' "${starter}"; then
  echo "Note: starter/converter.py still has unfinished exercises — testing structure only."
  # Structural checks while the learner works.
  grep -q 'def celsius_to_fahrenheit' "${starter}" && check "starter defines celsius_to_fahrenheit" "yes" || check "starter defines celsius_to_fahrenheit" "no"
  grep -q 'def main' "${starter}" && check "starter defines main" "yes" || check "starter defines main" "no"
else
  # Learner finished: hold the starter to the same strict standard.
  run_program_checks "${starter}"
  grep -q '__name__ == "__main__"' "${starter}" && check "starter has the main guard" "yes" || check "starter has the main guard" "no"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 049 lab

python: command not found

Use python3 explicitly, as every command in this lab does. On macOS and most Linux systems, bare python may be missing or point to an old version. Check with python3 --version.

The starter raises NotImplementedError when I run it

That is expected until you finish the exercises. Each unfinished function raises NotImplementedError on purpose so you cannot accidentally think an empty function "works." Replace each raise NotImplementedError(...) line with the real body described in the comment above it. Once all five exercises are done, the file runs like the reference.

ModuleNotFoundError: No module named 'converter'

Python imports a module by looking on its search path (sys.path), which does not include the examples/ subfolder by default. The import one-liners in this lab add it first:

python3 -c "import sys; sys.path.insert(0, 'examples'); from converter import celsius_to_fahrenheit; print(celsius_to_fahrenheit(100))"

Run this from the lab directory (the folder that contains examples/), not from inside examples/ itself. If you cd examples first, use sys.path.insert(0, '.') instead.

Importing the file runs the whole program

This is exactly the problem the main guard prevents. If importing your module prints output or exits, you either forgot if __name__ == "__main__": or wrote program-running code at the top level (outside any function) instead of inside main. Only the guarded sys.exit(main(sys.argv)) should trigger execution. This is why "importing vs running" matters: a well-guarded file can be imported to test its functions and run to do its job, and the two never interfere.

Testing without a human: how the tests stay non-interactive

This program takes its input from command-line arguments, never by prompting. That is deliberate: a program that reads input() from a human cannot be tested automatically, because a test has no one to type. Because the converter reads sys.argv, a test can simply run python3 examples/converter.py 100 C and check the output and exit code — no interaction required. If you extend the program, keep input on the command line (or read from a file) so it stays testable.

echo $? shows 0 after a bad input

Your main is not returning 1 on the error path, or you are not passing its return value to sys.exit(). The guard must read sys.exit(main(sys.argv)), and main must return 1 after printing an error. The exit code is how other programs detect that yours failed.

bash: tests/run_tests.sh: Permission denied

Run it through bash explicitly (as the README shows) rather than executing it directly: bash tests/run_tests.sh. You do not need to chmod +x anything.

A conversion looks wrong

Check operator precedence: the Celsius-to-Fahrenheit formula is celsius * 9 / 5 + 32, and Python evaluates * and / before +. Verify by hand with a known value — 100 °C is 212 °F, 0 °C is 32 °F.

Security notes

Security notes — Day 049 lab

  • What the program does: reads two command-line arguments, converts a number, and prints the result. It makes no network connections, reads and writes no files, and changes no settings. The test runner is equally self-contained and non-interactive.

  • Validate input; never execute it. The most important security habit in this lab is turning text into data safely. The converter uses float(), which can only ever produce a number or raise an error — it cannot run code. Never use eval() or exec() on input, no matter how tempting it looks for "evaluating" what a user typed: those functions execute the string as Python, so a malicious value could delete files or open a network connection. Parse and validate input into safe types at the boundary, exactly as parse_args does.

  • Fail loudly, not silently. On bad input the program prints a clear message to standard error and exits with a non-zero code. Silent failure — computing a wrong answer from bad input and reporting it confidently — is worse than a crash, because no one notices. Validating at the boundary prevents both.

  • Privileges: everything runs as your normal user. Nothing here needs sudo. As always in this course, if a script ever asks you to run it with elevated privileges, stop and read it first.

  • Reading before running: every file in this lab is short and commented. Read examples/converter.py and tests/run_tests.sh before running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.