JJoeven

Curriculum/Python

Logging and Secrets

Log with a list of dicts and levels. Read env vars with os.environ.get. Never print API keys. Redact them with ***.

intermediate18 min30 / 37

A log is a list of what happened. print shows text now. A log list you can filter, hide, and save. A secret is a value that must not leak: an API key, a token, a password. Read secrets from the environment. Never print them. Never put them in the prompt. Never put them in the transcript you send back to a model.

This lesson is Python: lists, strings, os.environ.get, and replace. It is also policy: assume a screenshot of the output will leave the building.

Read a key. Do not log it.
env varloadlog ***

Get the secret from the environment. Replace it with *** before you print or save a row.

Read a key. Do not log it.

print vs a log list

print is fine while you learn. It is weak in a real agent:

  • you cannot filter “errors only”
  • you cannot hide a key after the fact
  • you cannot send the same rows to a file later

A simple log is a list of dicts:

python
log = []
log.append({class="tok-s">"level": class="tok-s">"info", class="tok-s">"msg": class="tok-s">"agent start"})
print(log)

Each row has a level and a message. You can loop later and print only errors. You can json.dumps the list and write it with pathlib. That is a file log without a logging framework.

The stdlib logging module exists. It is useful on a laptop. Here, a list is visible and testable. Tests assert log[-1]["level"] == "error". They cannot easily assert on a print.

Levels in simple words

LevelMeaning
debugExtra detail while you build
infoNormal progress
warningOdd, but we continue
errorSomething failed

Pick one level per row. Do not mark everything as error. Then nothing stands out. Do not mark everything as debug. Then you will never look.

A production switch is “print warnings and errors only.” That is a filter over the same list: [row for row in log if row["level"] in ("warning", "error")].

Stamp a step number or run id when you have one. {"level": "info", "msg": "...", "step": 3} is easier to grep. Keep values JSON-safe: strings, numbers, bools, None. Error objects should be str(e) first.

os.environ.get

The environment is a set of names the computer keeps outside your file. os.environ.get("API_KEY") reads one name.

If the name is missing, .get returns None. You can pass a default: os.environ.get("API_KEY", "").

Joeven’s browser has no real secret store. On your machine, you set API_KEY in the environment. You do not paste keys into source files. You do not commit .env files that hold live keys. A placeholder default in a demo is fine if it is obviously fake.

python
import os

key = os.environ.get(class="tok-s">"API_KEY")
if not key:
    print(class="tok-s">"missing key")

Missing key should be a startup error, not a 401 after ten paid calls. Check once in main. Do not print the key when it is present. Print bool(key) or "key set" if you must.

Never print API keys

A key in a log is a leak. Screenshots, traces, and bug reports all copy the log. Assume someone else will read it. Support tickets include logs. Models that see a transcript will echo secrets if you put them there.

Do not do this:

python
print(class="tok-s">"using", api_key)

Do not put the key in the goal string. Do not put it in tool args that get logged. Headers belong in the HTTP client, not in print(headers) unless you redacted first.

Redaction

Redact means hide a secret by replacing it. A simple form: replace the key with ***.

python
def redact(text, secret):
    if secret and secret in text:
        return text.replace(secret, class="tok-s">"***")
    return text

Run redaction before you append to the log. Then even an error message that included the key is safe to print. If you print first and redact later, the leak already happened in the output panel.

Redact every secret you know: key, token, password. If you have several, loop. If a secret is empty, skip — "" in text is True for every string, and you would replace nothing useful or behave oddly. The if secret and guard matters.

A comment that says “do not log keys” does not hide them. Replace the key in code.

If you have several secrets, loop them through redact. If a new token appears only in an error message you did not expect, you cannot redact what you do not know. Avoid putting secrets in exception messages. Raise ValueError("missing API_KEY") not ValueError("bad key " + key). The tryit shows redaction because the demo intentionally concatenates the key. Real raise messages should never include it.

Levels are not a substitute for a trace. A trace is the transcript of the agent. A log is how your runtime talks to operators. Both must be redacted. Filter logs by level. Slice traces by step.

Common mistakes

  • Printing headers with Authorization.
  • Defaulting a missing key to a real key in source.
  • Redacting after print.
  • Using if not text: on a key and then logging the error that still contains the key.
  • Storing secrets in the transcript list.
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

The error row should contain ***, not sk-demo-not-real. Filter errors the same way you would in a dashboard.

How agents use this

Print while you learn, then store rows in a log list. Use levels so you can show errors first. Read API_KEY with os.environ.get. Never print the raw key. Replace it with *** before a row is saved. Traces go to files, other people, and sometimes back into a model — keep secrets out.

The executor should redact observations too. A tool that returns a URL with a signed query might leak a token. If you know the token, replace it. If you do not know it, do not log full URLs from unknown tools without a review.

Startup should fail closed: no key, no loop. That is an if not key: return in main, not a comment. The environment is how machines get secrets. Python’s job is to read them quietly and never write them back out.

Check your understanding

How should an API key appear in a log?