Running it and reading the logs
what the program tells you about itself
What it is
A log is what a program writes about itself while it runs. For debugging it matters more than a traceback: a traceback shows the moment of the crash, a log shows what was happening before.
For a bot or a server the log is the only way to find out what happened at all: you don't see the user's screen and you can't ask them again.
What it looks like in code
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger(__name__)
def fetch_weather(city: str) -> dict:
log.info("weather request: %s", city)
try:
data = call_api(city)
except TimeoutError:
log.warning("timeout for city %s, retrying", city)
data = call_api(city)
log.debug("API response: %s", data)
return dataOutput:
2026-10-14 13:02:11 INFO handlers.weather: weather request: Oslo
2026-10-14 13:02:12 WARNING handlers.weather: timeout for city Oslo, retryingFive levels
| Level | When | Visible by default |
|---|---|---|
DEBUG | details for debugging | no |
INFO | the normal course of work | yes |
WARNING | odd, but we carry on | yes |
ERROR | we failed to do what was asked | yes |
CRITICAL | we cannot carry on | yes |
The line level=logging.INFO means: show INFO and anything more serious.
To see DEBUG, the level is changed to logging.DEBUG — usually
temporarily, while hunting an error.
How to read a log
Bottom to top, up to the first surprise. The last lines are consequences; the interesting part is usually above.
Look at the times. A gap of several seconds between neighbouring lines shows where the program was waiting. It is the cheapest way to find out what is slow.
Look for a WARNING before an ERROR. The warning often names the real
cause, while the error is only its consequence.
Count the repeats. The same line a hundred times in a row is a loop that should not have looped, or retries that never end.
print or logging
print | logging |
|---|---|
| in a single script, while you debug | in a bot, a server, anything long-running |
| disappears when the program isn't in a terminal | goes to a file, survives a restart |
| no levels and no timestamps | levels, time, module name |
The rule: while you are hunting an error by hand — print (article 4.5).
Anything that stays in the code — logging.
What to tell the AI
Add logging to this function: INFO on entry with the key
arguments, WARNING for abnormal situations, ERROR on failure.
Don't log tokens or personal data.When the log already exists and you need to make sense of it:
Here is a chunk of the log. What happened here? What should
I look at first?
<log>When there isn't enough logging:
I don't understand why this function returns an empty list.
Where should I add logging to make it visible?
As few lines as possible.What to look for
Secrets in the logs. A token or password written to the log is the same secret in plain sight, only in a file that then gets forwarded. Check what is being logged before handing a log to anyone, the AI included.
A log with nothing in it. If the log is empty when an error occurs, logging was forgotten — and the only way to learn the cause is already gone.
A log with everything in it. The opposite extreme: DEBUG on everything
in production. The file grows, the important parts drown, the disk fills.
A message with no data. log.error("error") says nothing. A good entry
holds values: which city, which user, which response code.
Logging inside a loop over a thousand items. A thousand lines for one request. Log the outcome, not every step.
Cheat sheet
The log matters more than the traceback: it shows what came before.
Five levels: DEBUG · INFO · WARNING · ERROR · CRITICAL.
Read bottom to top until the first surprise; watch the time gaps.
A
WARNINGbefore anERRORis often the real cause.printwhile debugging,loggingfor anything that stays in the code.Always include values in the message. Never log secrets.