TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

Configuration typos must not become defaults: design principles

Continue in TT Lab

Summary

You test strict configuration parsing, removal of secrets, and a per-app snapshot.

Why this matters

Converting the string false to a bool gave True. In production, debug responses were switched on, and the status page printed the entire configuration dictionary. Environment variables are strings, so a type declaration alone does not make a value safe. When the configuration is read and how much of it is made public are also part of the application contract.

How it works

Validate the port, the time limit, the boolean, and the required token separately. The settings are read once from the given dictionary and copied, and an invalid value is never silently replaced with a default. Defaults apply only to missing values. Even if the input dictionary is modified after the app is created, the behavior of the app that was already created must not change. The public status keeps only the service name and the debug value.

문자열 사전 → 개별 타입/범위 검증 → 설정 스냅샷 → 공개 허용 필드

A worksheet for reading the contract and predicting failures

What follows is not an answer key to memorize an implementation but a step-by-step code review. Each change fragment deliberately breaks the contract. Note that the normal case may still pass after the change. Before running it, predict which input, exception, or state you would observe to expose the difference, and after implementing it, compare that prediction with the result.

1. Parse the boolean explicitly

parse_bool(value) returns a bool only for true or false, ignoring case. If there is surrounding whitespace or the value is not a string, it is ValueError.

Basis for the judgment: bool('false') is True. Compare against the two allowed strings directly.

Faulty change fragment to review:

bool(value)

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

2. Check the port range

parse_port(value) converts a string that has only ASCII digits to an int and returns it if it is 1–65535. Everything else is ValueError.

Basis for the judgment: a successful integer conversion does not mean the value is in the valid port range.

Faulty change fragment to review:

<= 65536

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

3. Make the time limit a finite value

parse_timeout(value) converts a string to a float and returns only finite values greater than 0 and at most 30. Everything else is ValueError.

Basis for the judgment: NaN behaves unexpectedly in ordinary comparisons, so check isfinite.

Faulty change fragment to review:

0 <= number <= 30

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

4. Reject a missing required secret

required_token(env) returns the stripped value when TOKEN is a string and is not empty after strip. A missing or empty value is ValueError.

Basis for the judgment: do not replace a missing required secret with a sample default.

Faulty change fragment to review:

return value

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

5. Apply defaults only to missing values

load_settings(env) is a dictionary with service=SERVICE of env or 'api' if missing, debug=parse_bool(DEBUG, or 'false' if missing), port=parse_port(PORT, or '8000' if missing), timeout=parse_timeout(TIMEOUT, or '5' if missing), and token=required_token. An empty SERVICE is ValueError.

Basis for the judgment: the default of get and 'value or default' differ in how they treat an empty string.

Faulty change fragment to review:

env.get("DEBUG","true")

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

6. Remove secrets from the public settings

public_settings(settings) is a new dictionary that has only service and debug. It does not modify the original.

Basis for the judgment: rather than masking part of the token value, use a contract in which the field itself is not exposed.

Faulty change fragment to review:

"debug":settings["debug"],"token":settings["token"]}

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

7. Separate outside changes from the settings

snapshot(env) returns the result of load_settings. Even if env is modified after the call, the returned settings do not change.

Basis for the judgment: separate the settings at app startup from an input dictionary that may change later.

Faulty change fragment to review:

return env

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

8. Check startup failure and the public response

create_app(env) reads the snapshot immediately, and if the settings are invalid it makes app creation fail with ValueError. GET /info returns only public_settings. Apps created with different env values do not share settings.

Basis for the judgment: validate at creation so that a configuration error does not first show up on the first request after the server has started.

Faulty change fragment to review:

return settings

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

What it looks like in the field

The environment is injected as an ordinary dictionary, so it does not depend on the real process-wide environment. This is not an example that implements an encrypted secret store, key rotation, or dynamic reloading. The token string is teaching input, and you never put a real production key into a lab Pod.

What you will do in the next lab

Eight steps lead to one runnable result. Parse the boolean explicitly → check the port range → make the time limit a finite value → reject a missing required secret → apply defaults only to missing values → remove secrets from the public settings → separate outside changes from the settings → check startup failure and the public response.

Each step checks actual return values, exceptions, and state changes, not the fact that a function or file exists. After you see the answer, deliberately change a boundary comparison or the cleanup code and check which tests fail. Explain why the earlier tests are kept in the next steps, and write down one operating condition that this lab does not guarantee.