Warm-up · Activity 1 of 7
Warm-up from the previous lesson: what does this print?
port = 8080
print(t"{port}".values)// A5.2 · ~30 min · Advanced
After this lesson you can read a TOML file or string with tomllib, predict the Python type of each value, and validate the result with errors that name the bad key.
You will be able to
Warm-up · Activity 1 of 7
port = 8080
print(t"{port}".values)Predict · Activity 2 of 7
import tomllib
data = tomllib.loads("port = 8080\ndebug = true\n")
print(data, type(data["port"]).__name__)Practice · Activity 3 of 7
import tomllib
with open("config.toml", ____) as f:
config = tomllib.load(f)
print(config["name"])Practice · Activity 4 of 7
import tomllib
doc = """
released = 2026-09-29
[[server]]
name = "a"
[[server]]
name = "b"
"""
data = tomllib.loads(doc)
print(type(data["released"]).__name__, type(data["server"]).__name__, data["server"][1])Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
import tomllib
config = tomllib.loads("workers = true\n")
workers = config["workers"]
if isinstance(workers, int):
print("workers:", workers + 1)
else:
print("not a number")Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
config.toml describes a small service: a title, a release date, a [database] table and two [[servers]] entries. main.py opens it in binary mode, loads it and reads values of several types. At the end it parses a broken document, with the same key twice, and prints the error message and line that TOMLDecodeError carries. Both files are in the editor: change config.toml and run again.
main.py
import tomllib
with open("config.toml", "rb") as f: # binary mode: tomllib decodes UTF-8 itself
config = tomllib.load(f)
print(config["title"])
print(config["database"])
print(type(config["released"]).__name__, config["released"].year)
for server in config["servers"]:
print(f"{server['name']}: {server['ip']}")
try:
tomllib.loads("port = 80\nport = 81\n")
except tomllib.TOMLDecodeError as err:
print("error:", err.msg, "at line", err.lineno)
config.toml
# Settings for the report service
title = "Report service"
released = 2026-09-29
[database]
host = "db.local"
port = 5432
enabled = true
[[servers]]
name = "alpha"
ip = "10.0.0.1"
[[servers]]
name = "beta"
ip = "10.0.0.2"
Run it with
python main.pyOutput
Report service
{'host': 'db.local', 'port': 5432, 'enabled': True}
date 2026
alpha: 10.0.0.1
beta: 10.0.0.2
error: Cannot overwrite a value at line 2Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercise 1 of 2
Fix both functions in main.py. read_port(path) returns server.port from a TOML file. read_price(path) returns the top-level price as an exact Decimal, so that price = 0.1 gives Decimal("0.1"), not the float 0.1. The starter opens the files in text mode, which tomllib refuses.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
tomllib.load wants a binary file: open(path, "rb"). No encoding argument in binary mode.
Nested tables are nested dicts: config["server"]["port"].
tomllib.load(f, parse_float=Decimal) calls Decimal with the text of each TOML float.
One way to solve it. Yours can look different and still pass the checks.
import tomllib
from decimal import Decimal
def read_port(path: str) -> int:
"""Return server.port from the TOML file at path."""
with open(path, "rb") as f:
config = tomllib.load(f)
return config["server"]["port"]
def read_price(path: str) -> Decimal:
"""Return price from the TOML file at path, as an exact Decimal."""
with open(path, "rb") as f:
config = tomllib.load(f, parse_float=Decimal)
return config["price"]
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import tomllib
from decimal import Decimal
def read_port(path: str) -> int:
"""Return server.port from the TOML file at path."""
with open(path, encoding="utf-8") as f:
config = tomllib.load(f)
return config["server"]["port"]
def read_price(path: str) -> Decimal:
"""Return price from the TOML file at path, as an exact Decimal."""
with open(path, encoding="utf-8") as f:
config = tomllib.load(f)
return config["price"]
test_main.py
from decimal import Decimal
from main import read_port, read_price
def write(name, text):
with open(name, "w", encoding="utf-8") as f:
f.write(text)
return name
def test_port():
"""server.port is read from the file"""
path = write("app.toml", '[server]\nhost = "0.0.0.0"\nport = 8080\n')
got = read_port(path)
assert got == 8080, f"read_port gave {got!r}, expected 8080"
def test_other_file():
"""Another file gives its own port"""
path = write("other.toml", "[server]\nport = 9000\n")
got = read_port(path)
assert got == 9000, f"read_port gave {got!r}, expected 9000"
def test_price_exact():
"""price = 0.1 comes back as Decimal('0.1')"""
path = write("shop.toml", "price = 0.1\n")
got = read_price(path)
assert isinstance(got, Decimal) and got == Decimal("0.1"), f"read_price gave {got!r}, expected Decimal('0.1')"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
Complete load_settings(text). Parse the TOML, then raise ValueError, with the key in the message, when: a key is not in ALLOWED; name is missing or not a non-empty str; port is not an int from 1 to 65535 (a bool does not count); debug is present but not a bool. Return {"name": ..., "port": ..., "debug": ...}, with debug False when missing.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
set(config) - ALLOWED gives the unknown keys. Raise if it is not empty.
config.get("port") returns None for a missing key, and isinstance(None, int) is False, so one check covers missing and wrong type.
Reject bool explicitly: not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535.
One way to solve it. Yours can look different and still pass the checks.
import tomllib
ALLOWED = {"name", "port", "debug"}
def load_settings(text: str) -> dict[str, object]:
"""Parse TOML settings and check them; raise ValueError on any problem."""
config = tomllib.loads(text) # TOMLDecodeError is a ValueError too
unknown = set(config) - ALLOWED
if unknown:
raise ValueError(f"unknown key: {sorted(unknown)[0]}")
name = config.get("name")
if not isinstance(name, str) or not name:
raise ValueError("name must be a non-empty string")
port = config.get("port")
if not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535:
raise ValueError("port must be an integer from 1 to 65535")
debug = config.get("debug", False)
if not isinstance(debug, bool):
raise ValueError("debug must be true or false")
return {"name": name, "port": port, "debug": debug}
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import tomllib
ALLOWED = {"name", "port", "debug"}
def load_settings(text: str) -> dict[str, object]:
"""Parse TOML settings and check them; raise ValueError on any problem."""
config = tomllib.loads(text)
# Check: no key outside ALLOWED; name a non-empty str; port an int
# (not a bool) from 1 to 65535; debug a bool, False when missing.
return config
test_main.py
from main import load_settings
def error_of(text):
try:
load_settings(text)
except ValueError as err:
return str(err)
return None
def test_valid():
"""A complete, valid config is returned"""
got = load_settings('name = "api"\nport = 8080\ndebug = true\n')
assert got == {"name": "api", "port": 8080, "debug": True}, f"load_settings gave {got!r}"
def test_debug_default():
"""debug defaults to False"""
got = load_settings('name = "api"\nport = 8080\n')
assert got == {"name": "api", "port": 8080, "debug": False}, f"load_settings gave {got!r}"
def test_missing_name():
"""A missing name raises ValueError that names the key"""
got = error_of("port = 8080\n")
assert got is not None and "name" in got, f"expected ValueError mentioning name, got {got!r}"
def test_port_bool():
"""port = true is rejected, although True is an int"""
got = error_of('name = "api"\nport = true\n')
assert got is not None and "port" in got, f"expected ValueError mentioning port, got {got!r}"
def test_port_text_and_range():
"""port = "80" and port = 70000 are rejected"""
got = error_of('name = "api"\nport = "80"\n'), error_of('name = "api"\nport = 70000\n')
assert None not in got, f"expected two ValueErrors, got {got!r}"
def test_unknown_key():
"""A misspelt key such as prot is reported"""
got = error_of('name = "api"\nport = 8080\nprot = 1\n')
assert got is not None and "prot" in got, f"expected ValueError mentioning prot, got {got!r}"
def test_bad_toml():
"""Invalid TOML raises ValueError too"""
got = error_of("name = api\n")
assert got is not None, "invalid TOML did not raise ValueError"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyimport tomllib
with open("app.toml", "w", encoding="utf-8") as f:
f.write('name = "demo"\n')
with open("app.toml", encoding="utf-8") as f:
config = tomllib.load(f)
What Python prints
TypeError: File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`Why, and the fix
tomllib.load reads bytes and decodes them as UTF-8 itself, because TOML files are always UTF-8. Open the file with open(path, "rb") and leave out encoding=. If you already hold the text as a str, call tomllib.loads(text) instead.
import tomllib
data = b'name = "demo"'
config = tomllib.loads(data)
What Python prints
TypeError: Expected str object, not 'bytes'Why, and the fix
loads takes a str. For bytes, from a network or a zip file for example, decode first, tomllib.loads(data.decode("utf-8")), or wrap them for load: tomllib.load(io.BytesIO(data)).
import tomllib
config = tomllib.loads("name = unquoted")
What Python prints
tomllib.TOMLDecodeError: Invalid value (at line 1, column 8)Why, and the fix
In TOML every string needs quotes: name = "unquoted". Only numbers, true, false, dates, arrays and tables are written bare. The line and column in the message point at the first character the parser could not read.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.