Programming with PythonPython Setup and First Programs › Day 47

Hands-on lab — Day 47: Input, Output, and f-strings

Commands

Setup

cd labs/sections/programming-with-python/day-047-input-output-and-f-strings

Run

python3 examples/io_demo.py 5 7
echo "5 7" | python3 examples/io_demo.py
python3 starter/io_demo.py 8 3

Test

bash tests/run_tests.sh

File tree

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

Lab README

Day 047 lab — A Friendly CLI Greeter/Calculator

Lesson

Purpose

Day 47's lesson explains how a program talks to people and to pipes. This lab makes it concrete: you build io_demo.py, a small command-line tool that reads two numbers either from a command-line argument or from standard input, converts them safely, and prints a neatly f-string-formatted, aligned report — sending errors to standard error and exiting non-zero on bad input. It is a miniature of every well-behaved Unix command-line tool.

Learning objectives

  • Read input from sys.argv and from sys.stdin, and choose which fits.
  • Convert text input to numbers safely with float(), and see why input() returning a string matters.
  • Format numbers into aligned, fixed-decimal columns with f-string format specs.
  • Route results to standard output and errors to standard error, and exit with a non-zero code on failure.
  • Test a non-interactive program by driving it with pipes and arguments.

Prerequisites

  • The Day 47 lesson (read it first — it explains every concept this lab uses).
  • Days 43–46: a working Python 3, variables, strings, and numbers.
  • Day 10: the shell and pipes.
  • A terminal: Terminal.app (macOS), any terminal (Linux), or PowerShell/WSL (Windows).

Supported operating systems

  • macOS — fully supported (tested on macOS with Python 3.14).
  • Linux — fully supported (any distribution with Python 3 and bash).
  • Windows — run the Python program directly in PowerShell (python examples\io_demo.py 5 7), or run everything unmodified inside WSL.

Hardware requirements

Any computer that runs Python 3. The lab does only trivial arithmetic and text formatting; it needs no particular RAM, disk, or GPU.

Required software

  • python3 3.6 or newer (f-strings and their format specs). Check with python3 --version.
  • bash to run the test script (preinstalled on macOS and Linux).

Free and open-source options

Everything in this lab is free and open source: Python and its standard library, bash, and the standard shell utilities the tests use. No account, API key, or purchase is needed, and nothing runs over the network.

Installation

None beyond Python itself. Clone the repository (or copy this directory) and change into it:

cd labs/sections/programming-with-python/day-047-input-output-and-f-strings

If python3 --version prints a version, you are ready. If not, see requirements/README.md.

File structure

day-047-input-output-and-f-strings/
├── README.md                     ← you are here
├── metadata.yml                  ← machine-readable lab metadata
├── starter/
│   ├── io_demo.py                ← YOUR working file (5 numbered exercises)
│   └── io-worksheet.md           ← worksheet for the practice assignment
├── examples/
│   └── io_demo.py                ← completed reference implementation
├── tests/
│   └── run_tests.sh              ← automated, non-interactive checks
├── expected-output/
│   ├── sample-run.txt            ← real captured run (macOS, Python 3.14)
│   └── FIELDS.md                 ← required output fields 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 result first — both input paths
python3 examples/io_demo.py 5 7
echo "5 7" | python3 examples/io_demo.py

## 2. Your task: complete the five exercises in the starter, then run it
python3 starter/io_demo.py 8 3
echo "8 3" | python3 starter/io_demo.py

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

What the commands do

  • python3 examples/io_demo.py 5 7 — runs the reference in argument mode: it joins the command-line arguments, splits them into two numbers, converts with float(), and prints the aligned report.
  • echo "5 7" | python3 examples/io_demo.py — runs the same program in stdin mode: with no arguments it reads the piped text from sys.stdin. The output is byte-for-byte identical to argument mode.
  • python3 starter/io_demo.py 8 3 — runs your in-progress version; until you finish the exercises it still contains FILL_ME_IN placeholders.
  • bash tests/run_tests.sh — runs concept checks (python3 -c), then drives the reference program with pipes and arguments to verify aligned output, and confirms bad input goes to stderr with a non-zero exit. It checks your starter strictly once every FILL_ME_IN is gone, and structurally before that.

Expected output

See expected-output/sample-run.txt — a real captured run. Good input prints an aligned report to standard output:

Input values
  a            5.00
  b            7.00
Results
  sum         12.00
  product     35.00
  mean         6.00

Bad input prints nothing to stdout, writes an error: line to standard error, and exits with code 1. expected-output/FIELDS.md lists exactly which fields must appear.

Validation steps

  1. Run python3 examples/io_demo.py 5 7 and confirm the aligned report above.
  2. Run echo "5 7" | python3 examples/io_demo.py and confirm identical output.
  3. Run echo "5 hello" | python3 examples/io_demo.py; echo $? and confirm an error: line and an exit code of 1.
  4. Complete starter/io_demo.py (replace every FILL_ME_IN) so it matches the reference, then run the tests.

Tests

bash tests/run_tests.sh

Expected final line: 19 checks, 0 failure(s). while the starter is unfinished (4 concept checks, 11 strict checks on the reference, 4 structural checks on the starter), rising to 26 checks, 0 failure(s). once your starter passes strict mode. The command exits 0 on success and non-zero on any failure, so it can run in CI. It is fully non-interactive — every input is supplied via a pipe or an argument.

Cleanup

Nothing to clean up: the program writes no files and makes no network calls. To reset your work, restore the starter from git: git checkout -- starter/io_demo.py.

Troubleshooting

See troubleshooting.md for the full list (string-vs-number conversion, the hanging-input() trap, testing interactive programs via pipes, stderr routing, exit codes, ragged columns, Windows notes).

Security notes

See security.md. Short version: never eval() input, validate and convert everything, and think before printing sensitive values. The scripts make no network calls, write no files, and need no elevated privileges.

Extension exercises

  1. Make io_demo.py a streaming filter: read sys.stdin line by line, treat each line as one a b pair, and print one formatted result row per line so printf '5 7\n2 3\n' | python3 io_demo.py prints two rows.
  2. Add a , thousands separator to the sum column so 1000000 2000000 prints its sum as 3,000,000.00.
  3. Make a malformed line print a warning to stderr and be skipped, without aborting the whole run — one bad line should not lose the good ones.
  • Previous day: Day 46 — Numbers, Math, and Precision (labs/sections/programming-with-python/day-046-numbers-math-and-precision/).
  • Next day: Day 48 — Reading Error Messages and Debugging (labs/sections/programming-with-python/day-048-reading-error-messages-and-debugging/, to be written).

Expected output

FIELDS.md

# Required output fields (all platforms)

`sample-run.txt` in this directory is a real captured run on the authoring
machine (macOS, Python 3.14, 2026-07-12). The behavior is identical on Linux
and inside WSL — Python's formatting and stream handling are the same
everywhere; only the shell prompt (`$`) may look different.

## On good input (`5 7`, by argument or by pipe)

A correct run of `io_demo.py` prints, on **standard output**, in order:

1. `Input values`
2. `  a` followed by the first number, right-aligned, two decimals (`5.00`)
3. `  b` followed by the second number, right-aligned, two decimals (`7.00`)
4. `Results`
5. `  sum` followed by the sum, two decimals (`12.00`)
6. `  product` followed by the product, two decimals (`35.00`)
7. `  mean` followed by the mean, two decimals (`6.00`)

The two number columns are right-aligned to a fixed width, so the decimal
points line up regardless of how many digits each value has. Argument mode
(`python3 io_demo.py 5 7`) and pipe mode (`echo "5 7" | python3 io_demo.py`)
produce byte-for-byte identical output.

## On bad input (e.g. `5 hello`, or only one number)

- **Nothing** is printed to standard output.
- A single line beginning `error:` is printed to **standard error**.
- The process exits with a **non-zero** code (`1`); `echo $?` confirms it.

This split — results on stdout, errors on stderr, non-zero exit on failure — is
what lets the tool sit safely in a shell pipeline.

sample-run.txt

$ python3 examples/io_demo.py 5 7
Input values
  a            5.00
  b            7.00
Results
  sum         12.00
  product     35.00
  mean         6.00

$ echo "5 7" | python3 examples/io_demo.py
Input values
  a            5.00
  b            7.00
Results
  sum         12.00
  product     35.00
  mean         6.00

$ echo "5 hello" | python3 examples/io_demo.py
error: both inputs must be numbers (got '5 hello')
$ echo $?
1

Source files

examples/io_demo.py (2339 bytes)
#!/usr/bin/env python3
"""Day 047 lab — a friendly CLI greeter/calculator (reference solution).

Reads two numbers EITHER from a command-line argument OR from standard input,
so it is testable non-interactively:

    python3 io_demo.py 5 7
    echo "5 7" | python3 io_demo.py

It converts the input safely (a clear error on bad input), computes a small
stats line, and prints a nicely f-string-formatted, aligned report. On bad
input it writes a message to STANDARD ERROR and exits with a non-zero code, so
the failure is visible to a shell or a calling script.
"""
import sys


def read_raw() -> str:
    """Return the raw input text: the command-line argument if given,
    otherwise everything piped in on standard input."""
    if len(sys.argv) > 1:
        # Join all positional args so both `io_demo.py 5 7` and
        # `io_demo.py "5 7"` work.
        return " ".join(sys.argv[1:])
    return sys.stdin.read()


def parse_pair(raw: str) -> tuple[float, float]:
    """Split the raw text into exactly two numbers, converting safely.

    Raises ValueError with a clear message if there are not exactly two
    whitespace-separated tokens or if either token is not a number.
    """
    parts = raw.split()
    if len(parts) != 2:
        raise ValueError(f"expected two numbers, got {len(parts)}")
    try:
        return float(parts[0]), float(parts[1])
    except ValueError:
        # Re-raise with the original text so the message is actionable.
        raise ValueError(f"both inputs must be numbers (got {raw.strip()!r})")


def format_report(a: float, b: float) -> str:
    """Build the aligned, f-string-formatted report as one string."""
    total = a + b
    product = a * b
    mean = total / 2
    lines = [
        "Input values",
        f"  {'a':<9}{a:>8.2f}",
        f"  {'b':<9}{b:>8.2f}",
        "Results",
        f"  {'sum':<9}{total:>8.2f}",
        f"  {'product':<9}{product:>8.2f}",
        f"  {'mean':<9}{mean:>8.2f}",
    ]
    return "\n".join(lines)


def main() -> int:
    raw = read_raw()
    try:
        a, b = parse_pair(raw)
    except ValueError as err:
        # Diagnostics go to STDERR, never stdout, so a pipeline stays clean.
        print(f"error: {err}", file=sys.stderr)
        return 1
    print(format_report(a, b))
    return 0


if __name__ == "__main__":
    sys.exit(main())
metadata.yml (621 bytes)
lesson_id: D047
day: 47
kind: python-program
languages: [python]
setup_commands:
  - cd labs/sections/programming-with-python/day-047-input-output-and-f-strings
run_commands:
  - python3 examples/io_demo.py 5 7
  - echo "5 7" | python3 examples/io_demo.py
  - python3 starter/io_demo.py 8 3
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/io_demo.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, bash tests/run_tests.sh → 19 checks, 0 failures'
requirements/README.md (1134 bytes)
# Dependencies — Day 047 lab

**Only Python 3 and a POSIX shell.** This lab has no installable dependencies:

- `python3` (version 3.6 or newer — f-strings and their format specs work on
  every supported Python; the authoring machine used Python 3.14). Check with
  `python3 --version`.
- `bash` (preinstalled on macOS and every mainstream Linux distribution) to run
  the test script.
- Standard shell utilities used by the tests only: `echo`, `printf`, `grep`,
  `mktemp` — all part of the base system.

The program itself imports only `sys` from the Python standard library, so there
is deliberately no `requirements.txt` here. Everything runs offline, needs no
account or API key, and needs no elevated privileges.

## Installing Python (if needed)

- **macOS:** `brew install python` (Homebrew) or download from the official
  python.org installer. macOS may already ship a `python3`.
- **Linux:** `sudo apt install python3` (Debian/Ubuntu) or the equivalent for
  your distribution; most ship Python 3 already.
- **Windows:** install Python from the Microsoft Store or python.org, or use WSL
  and follow the Linux path.
starter/io_demo.py (3164 bytes)
#!/usr/bin/env python3
"""Day 047 lab starter — complete the five numbered exercises below.

Goal: read two numbers (from a command-line argument OR from standard input),
convert them safely, and print an aligned, f-string-formatted report. On bad
input, write an error to STANDARD ERROR and exit with a non-zero code.

See the finished target first:
    python3 examples/io_demo.py 5 7
    echo "5 7" | python3 examples/io_demo.py

Then complete each exercise, replacing every "FILL_ME_IN", and run:
    bash tests/run_tests.sh

The sentinel FILL_ME_IN below is how the test knows the lab is unfinished.
"""
import sys

FILL_ME_IN = "FILL_ME_IN"


def read_raw() -> str:
    """Exercise 1 — read the input from EITHER the command line OR stdin.

    If arguments were given (len(sys.argv) > 1), join sys.argv[1:] with a
    single space and return that string. Otherwise, return sys.stdin.read().
    Reading both ways is what makes the program testable without a keyboard.
    Replace the return below with that logic.
    """
    return FILL_ME_IN


def parse_pair(raw: str) -> tuple[float, float]:
    """Exercises 2 and 4 — turn the raw text into exactly two numbers, safely.

    Exercise 2 (convert): split raw into tokens with raw.split(); if there are
      not exactly two tokens, raise ValueError(f"expected two numbers, got
      {len(parts)}").
    Exercise 4 (handle bad input): convert with float(parts[0]) and
      float(parts[1]) inside a try/except ValueError, and in the except branch
      raise ValueError(f"both inputs must be numbers (got {raw.strip()!r})").
    Remember: input text is a str — float() is what makes it a number.
    """
    parts = raw.split()
    # ... your Exercise 2 length check goes here ...
    # ... your Exercise 4 try/except conversion goes here ...
    return float(parts[0]), float(parts[1])  # replace with the safe version


def format_report(a: float, b: float) -> str:
    """Exercise 3 — format each value right-aligned to width 8, two decimals.

    Each value below currently prints with no formatting. Give every number an
    f-string spec of the form {value:>8.2f} so the two columns line up and show
    exactly two decimal places (compare against examples/io_demo.py).
    """
    total = a + b
    product = a * b
    mean = total / 2
    lines = [
        "Input values",
        f"  {'a':<9}{a}",          # replace {a} with {a:>8.2f}
        f"  {'b':<9}{b}",          # replace {b} with {b:>8.2f}
        "Results",
        f"  {'sum':<9}{total}",    # replace {total} with the aligned spec
        f"  {'product':<9}{product}",
        f"  {'mean':<9}{mean}",
    ]
    return "\n".join(lines)


def main() -> int:
    raw = read_raw()
    try:
        a, b = parse_pair(raw)
    except ValueError as err:
        # Exercise 5 — write the error to STANDARD ERROR (not stdout) so it
        # never pollutes a pipeline, then signal failure with a non-zero code.
        # Add  file=sys.stderr  to the print below.
        print(f"error: {err}")  # <- add file=sys.stderr
        return 1
    print(format_report(a, b))
    return 0


if __name__ == "__main__":
    sys.exit(main())
starter/io-worksheet.md (2282 bytes)
# Day 047 worksheet — Input, Output, and f-strings

Fill this in from your own runs as you complete `io_demo.py`. Copy real values
straight from your terminal — the point is to *see* the behavior, not guess it.

## 1. Proof that `input()` returns a string

Run this in a Python shell (`python3`), type `5` when prompted, and record what
you see:

```python
x = input("x: ")
type(x)      # what does this report?  __________________
x            # what value is x?         __________________
x + x        # what does this print?    __________________  (why is it not 10?)
```

- `type(input("x: "))` reports: `______________________`
- Typing `5` gives the value: `______________________`
- One sentence, in your own words, on why `input()` returns text and not a number:
  `______________________________________________________________________`

## 2. The format spec you used

Write the exact f-string spec you used to right-align a number to two decimals
in a fixed-width column, and label each part:

- Spec: `f"{value:__________}"`
- The alignment character is `____` and it means `______________________`
- The width is `____` and it means `______________________`
- The `.2f` part means `______________________`

## 3. What your finished program prints

Run your completed program on the input `8 3` **both ways** and paste the output:

```text
$ python3 starter/io_demo.py 8 3
______________________________________________
______________________________________________
______________________________________________
______________________________________________
______________________________________________
______________________________________________

$ echo "8 3" | python3 starter/io_demo.py
(should be identical to the line above)
```

## 4. Why errors go to standard error

In two or three sentences, explain why `io_demo.py` prints its error message to
**standard error** instead of standard output, and what would go wrong in a
pipeline like `python3 io_demo.py | sort` if the error went to standard output
instead:

```
____________________________________________________________________________
____________________________________________________________________________
____________________________________________________________________________
```
tests/run_tests.sh (5505 bytes)
#!/usr/bin/env bash
# Tests for the Day 047 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Verifies that io_demo.py reads a pair of numbers from EITHER a command-line
# argument OR standard input, prints an aligned, two-decimal report, and — on
# bad input — writes a message to STDERR and exits non-zero. The reference is
# always held to a strict behavioral standard. The learner's starter is checked
# strictly once every FILL_ME_IN placeholder is gone, and structurally (does it
# still parse, does it still contain the scaffolding) before that.
#
# No network, no interactivity: every input is supplied via arg or pipe.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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
}

# Full behavioral checks — run against any COMPLETE program.
run_strict_checks() {
  local script="$1" arg_out stdin_out bad_code bad_stderr bad_stdout tmp_err tmp_out

  echo "Testing ${script} (strict behavior) ..."
  tmp_err="$(mktemp)"
  tmp_out="$(mktemp)"

  # 1. Argument mode: `io_demo.py 5 7` exits 0.
  if arg_out="$(python3 "${script}" 5 7 2>/dev/null)"; then
    check "runs with command-line arguments (exit 0)" "yes"
  else
    check "runs with command-line arguments (exit 0)" "no"
  fi

  # 2. Stdin mode: `echo "5 7" | io_demo.py` exits 0.
  if stdin_out="$(echo "5 7" | python3 "${script}" 2>/dev/null)"; then
    check "runs with piped stdin (exit 0)" "yes"
  else
    check "runs with piped stdin (exit 0)" "no"
  fi

  # 3. The two input modes must agree.
  if [ "${arg_out}" = "${stdin_out}" ]; then
    check "argument and stdin modes produce identical output" "yes"
  else
    check "argument and stdin modes produce identical output" "no"
  fi

  # 4. Report structure and aligned, two-decimal values.
  echo "${arg_out}" | grep -q '^Input values$' && check "prints 'Input values' header" "yes" || check "prints 'Input values' header" "no"
  echo "${arg_out}" | grep -q '^Results$' && check "prints 'Results' header" "yes" || check "prints 'Results' header" "no"
  echo "${arg_out}" | grep -qE '(^| )12\.00$' && check "sum of 5 and 7 shown as 12.00 (two decimals)" "yes" || check "sum of 5 and 7 shown as 12.00 (two decimals)" "no"
  echo "${arg_out}" | grep -qE '(^| )35\.00$' && check "product shown as 35.00" "yes" || check "product shown as 35.00" "no"
  echo "${arg_out}" | grep -qE '(^| )6\.00$' && check "mean shown as 6.00" "yes" || check "mean shown as 6.00" "no"

  # 5. Bad input -> non-zero exit, a message on STDERR, and nothing on stdout.
  echo "5 hello" | python3 "${script}" 1>"${tmp_out}" 2>"${tmp_err}"
  bad_code=$?
  bad_stderr="$(cat "${tmp_err}")"
  bad_stdout="$(cat "${tmp_out}")"
  rm -f "${tmp_err}" "${tmp_out}"

  [ "${bad_code}" -ne 0 ] && check "bad input exits non-zero" "yes" || check "bad input exits non-zero" "no"
  [ -n "${bad_stderr}" ] && check "bad input writes a message to stderr" "yes" || check "bad input writes a message to stderr" "no"
  [ -z "${bad_stdout}" ] && check "bad input prints nothing to stdout" "yes" || check "bad input prints nothing to stdout" "no"
}

# Structural checks — run against an UNFINISHED starter.
run_structural_checks() {
  local script="$1"
  echo "Testing ${script} (structure only) ..."
  python3 -c 'import sys; compile(open(sys.argv[1]).read(), sys.argv[1], "exec")' "${script}" 2>/dev/null && check "starter is valid Python (parses)" "yes" || check "starter is valid Python (parses)" "no"
  grep -q 'sys.argv' "${script}" && check "starter references sys.argv (reading an argument)" "yes" || check "starter references sys.argv (reading an argument)" "no"
  grep -q 'sys.stdin' "${script}" && check "starter references sys.stdin (reading a pipe)" "yes" || check "starter references sys.stdin (reading a pipe)" "no"
  grep -q 'sys.stderr' "${script}" && check "starter references sys.stderr (error channel)" "yes" || check "starter references sys.stderr (error channel)" "no"
}

# --- python -c sanity checks (the concepts the lab teaches) ---------------
echo "Concept checks (python3 -c) ..."
printf '5\n' | python3 -c 'import sys; assert type(sys.stdin.readline().rstrip("\n")) is str' && check "a line read from input is a str" "yes" || check "a line read from input is a str" "no"
python3 -c 'assert f"{7.5:>10.2f}" == "      7.50"' && check "f-string {7.5:>10.2f} right-aligns to width 10" "yes" || check "f-string {7.5:>10.2f} right-aligns to width 10" "no"
python3 -c 'assert f"{1234567.891:,.2f}" == "1,234,567.89"' && check "f-string thousands separator works" "yes" || check "f-string thousands separator works" "no"
python3 -c 'assert "5" + "7" == "57" and int("5") + int("7") == 12' && check "converting input turns concatenation into addition" "yes" || check "converting input turns concatenation into addition" "no"
echo

run_strict_checks "${lab_dir}/examples/io_demo.py"
echo

# The starter ships with FILL_ME_IN placeholders. Once the learner has replaced
# every one, hold their script to the same strict standard.
if grep -q 'FILL_ME_IN' "${lab_dir}/starter/io_demo.py"; then
  echo "Note: starter/io_demo.py still has unfinished exercises (FILL_ME_IN)."
  run_structural_checks "${lab_dir}/starter/io_demo.py"
else
  run_strict_checks "${lab_dir}/starter/io_demo.py"
fi

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

Troubleshooting

Troubleshooting — Day 047 lab

The two numbers concatenate instead of adding (I get 57, not 12)

input() and every item in sys.argv are strings, so "5" + "7" glues them into "57". Convert each to a number first with float(...) (or int(...) for whole numbers) before doing arithmetic. This is the single most common beginner bug in Python, and the whole reason parse_pair calls float().

The program hangs and prints nothing

You probably called input() in a context with no keyboard. When a program is run inside an automated test or a pipeline with nothing piped in, input() waits forever for a human who is not there. This lab avoids the trap by reading sys.argv when an argument is present and sys.stdin otherwise — neither of which blocks on a missing keyboard. If you press Ctrl+D (macOS/Linux) it sends "end of input" and unblocks a waiting sys.stdin.read().

Testing an interactive program without typing

You do not need to type anything to test a program that reads standard input — feed it with a pipe or a here-string:

echo "5 7" | python3 examples/io_demo.py        # pipe
python3 examples/io_demo.py <<< "5 7"            # bash here-string
printf '5 7\n' | python3 examples/io_demo.py     # printf for exact bytes
python3 examples/io_demo.py 5 7                   # or just pass arguments

This is exactly how the test script drives the program, which is why the tests are fully non-interactive.

My error message shows up mixed into the data / gets captured by a pipe

You printed the error to standard output. Diagnostics must go to standard error: print(f"error: {err}", file=sys.stderr). To watch the two channels separately:

python3 examples/io_demo.py 5 hello 1>/tmp/out.txt 2>/tmp/err.txt
cat /tmp/out.txt   # should be empty on bad input
cat /tmp/err.txt   # the error line

echo $? shows 0 after bad input

You reported the error but did not signal failure. Return a non-zero code: call sys.exit(1) (or, as in the reference, return 1 from main() and sys.exit(main())). Only a non-zero exit tells a shell or CI that the run failed.

The columns are ragged / decimals do not line up

A number without a format spec prints at its natural width, so columns wander. Give every value in a column the same fixed-width, fixed-decimal spec, e.g. {value:>8.2f}> right-aligns, 8 sets the width, .2f fixes two decimals. Mixing widths between rows is the usual cause.

command not found: python3

Python 3 is not on your PATH. Install it (see requirements/README.md) or try python --version — on some systems the command is python. Never assume; check the version, because python sometimes still means Python 2.

Windows: bash is not recognized

Use WSL (wsl --install, then open Ubuntu and run the commands there), or run the Python program directly in PowerShell (python examples\io_demo.py 5 7) and skip the bash test script.

Security notes

Security notes — Day 047 lab

  • Never eval() input. The most dangerous mistake with input() (or with sys.argv, or with piped data) is passing it to eval(), which executes the text as Python code. eval(input()) lets whoever supplies the input run any command on your machine — delete files, open a network connection, anything. This lab deliberately turns text into numbers with float(), which parses a value and nothing more. Whenever you need a number from input, reach for int() or float(), never eval().

  • Validate and convert every input. Input is data, not instructions. The program checks that it received exactly two whitespace-separated tokens and that both convert to numbers, and it rejects anything else with a clear error. Treating all input as untrusted until validated is the habit behind every defense against injection attacks.

  • Mind what you print, and where. Output is where data leaves your program. Printing a secret for debugging is the classic way a password or key leaks into a terminal, a log, or a screen-share. Standard error is captured by logging systems just as readily as standard output, so "I only printed it to stderr" is no protection. Before every print(), consider who can see that channel.

  • What these scripts do: read a pair of numbers, compute a few statistics, and print them. They make no network connections, write no files, and change no settings. They need no elevated privileges — nothing here should ever require sudo.

  • Read before you run. Both io_demo.py files are short and commented; read them before executing. Running unread scripts is one of the most common ways developers get compromised, and this course's rule is that every lab file is small enough to read and understand first.