Skip to content
beginner

Python Logging: Add Levels and Useful Messages to Small Applications

Your script works. That is the problem. Somewhere between the first print("here") and the fifth print("still here"), your console turned into a wall of…

Published 2026-10-02Updated 2026-10-049 min read
Close-up of a blooming purple flower with lush green backdrop, highlighting nature's beauty and growth.
Close-up of a blooming purple flower with lush green backdrop, highlighting nature's beauty and growth. Photo by Rafael Rodrigues on Pexels.

Your script works. That is the problem. Somewhere between the first print("here") and the fifth print("still here"), your console turned into a wall of text where a routine progress note and a broken assumption look exactly the same. You cannot filter it. You cannot turn it off without deleting lines you will want back tomorrow.

Python's logging module fixes that with one idea: a log message is a print statement that carries a severity and an off switch. You decide what to see without editing the code that produces it. This tutorial gets you from scattered prints to leveled log messages in a small script, using only the standard library and one configuration call.

Why Print Statements Stop Scaling

Print is not a bad tool. For a five-line script you will delete in an hour, print() is faster, and there is no shame in it. Logging starts paying off when the script grows past a few functions, or when it runs unattended and you are not there to watch it.

Three limits show up first:

  • Print has no severity. A progress note and a broken assumption render identically. Your eyes have to do the sorting.
  • Print has no off switch. Silencing debug output means editing or commenting out code, then remembering to put it back.
  • Print output is not labeled. When a helper function and your main loop both print, you cannot tell which part of the script spoke.

The decision rule is simple: keep print for throwaway inspection, and move to logging when you need to filter, label, or keep the output. If you have already read our debugging guide, this is the natural next step — same instinct of watching values, now with structure attached.

Your First Log Message in Four Lines

Create a file called app.py:

import logging

logging.warning("Disk space is getting low")
logging.info("Processed 12 files")

Run it:

python app.py

Expected output:

WARNING:root:Disk space is getting low

Notice what happened. The warning appeared. The info call printed nothing at all — no error, no traceback, just silence.

That is not a bug. The default threshold is WARNING, so anything below it is dropped before it reaches the console. This is the first real mental model: a log call is a request, and the logger decides whether to honor it.

Knowledge check

Check your understanding

Answer this question before you continue.

With the default configuration, what appears when a script calls `logging.warning("Disk space is getting low")` and then `logging.info("Processed 12 files")`?
Output Prediction

Focus: Predict which direct logging calls appear when Python uses its default WARNING threshold.

The Five Levels and What Each One Is For

Python ships with five standard levels, in increasing severity. Each has a matching method on the logger.

LevelUse it whenWhat it means in a normal run
DEBUGYou are chasing a bug and want variable valuesShould be invisible; if you see it, someone turned verbosity up
INFOSomething expected happened: "started processing 12 files"Normal operation, worth recording
WARNINGSomething unexpected but survivable: "config key missing, using default"The script continued, but you should look
ERRORA real failure: "could not write report"Something did not work
CRITICALThe program cannot continue at allStop and fix before running again

Internally each level is just a number — DEBUG is 10, INFO is 20, and so on up to CRITICAL at 50. You do not need to memorize the numbers. They exist so the logger can compare "how severe is this message" against "how severe am I willing to show" with a simple greater-than check.

The level should describe the event, not your mood about it. A routine run summary belongs at INFO because you would want it in a normal report. A variable dump you only need while chasing a bug belongs at DEBUG because it should stay invisible until you ask for it. When you are genuinely torn between two levels, ask which one you would want to see on a normal day: if the answer is "yes, always," it is probably INFO; if the answer is "only when something looks wrong," it is probably DEBUG.

That rule keeps you consistent with the later warning about noisy logs. Downgrading everything to DEBUG to be safe does not protect information — it just hides the routine events you actually wanted to see.

Knowledge check

Check your understanding

Answer this question before you continue.

A configuration key is missing, but the script can continue by using a default. Which level best matches this event?
Single Choice

Focus: Choose a logging level that matches an unexpected condition the script can survive.

Setting the Level with basicConfig

A compact matrix compares two logging thresholds: with WARNING, DEBUG and INFO are hidden while WARNING, ERROR, and CRITICAL appear; with DEBUG, all five levels appear.
A lower threshold reveals more messages without changing the log calls in your code.

One call changes everything. Update app.py:

import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug("Loaded 3 config keys")
logging.info("Processed 12 files")
logging.warning("Disk space is getting low")
logging.error("Could not write report")
logging.critical("Shutting down")

Run it again:

python app.py

Expected output:

DEBUG:root:Loaded 3 config keys
INFO:root:Processed 12 files
WARNING:root:Disk space is getting low
ERROR:root:Could not write report
CRITICAL:root:Shutting down

All five messages now appear. The threshold moved from WARNING down to DEBUG, so nothing gets dropped.

The output is still a bit bare, though. You get the level name and the message, but no timestamp. Add a format argument:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

logging.info("Processed 12 files")
logging.warning("Disk space is getting low")

Expected output:

2024-06-14 09:12:03,441 INFO Processed 12 files
2024-06-14 09:12:03,441 WARNING Disk space is getting low

The %(asctime)s placeholder inserts a timestamp, %(levelname)s inserts the severity, and %(message)s inserts your text. That timestamp is the one addition worth making early — it answers "when did this happen" without you having to guess from context.

Note: Call basicConfig() once, near the top of your script, before any log calls. If you call it after logging has already been used, or call it twice, the second call is silently ignored and you will wonder why nothing changed.

Knowledge check

Check your understanding

Answer this question before you continue.

Which calls produce output with this configuration?
Output Prediction

Focus: Determine which messages pass an INFO logging threshold.

logging.basicConfig(level=logging.INFO)
logging.debug("Loaded config")
logging.info("Started run")
logging.warning("Using default")

Logging a Variable Without Breaking Your Message

The most common beginner mistake when moving from print to logging is reaching for an f-string:

logging.debug(f"processed {done} of {total}")

This works, but it builds the string every single time — even when DEBUG is turned off and the message will be thrown away. In a tight loop, that is wasted work.

The idiomatic style passes the values as arguments:

import logging

logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(message)s")

done = 7
total = 12

logging.debug("processed %s of %s", done, total)
logging.info("processed %s of %s", done, total)

Expected output:

DEBUG processed 7 of 12
INFO processed 7 of 12

The %s placeholders are filled in only if the message passes the level check. When DEBUG is off, the formatting never happens. Same output, less work.

This is the same "watch the value change" habit from debugging — you are still inspecting done and total — but now the observation carries a label and a severity, so future-you knows whether it was routine or suspicious.

Knowledge check

Check your understanding

Answer this question before you continue.

You want to log `done` and `total` without formatting the message when DEBUG is disabled. Which replacement follows the article's recommended style?
Debugging

Focus: Replace eager f-string interpolation with logging's deferred argument formatting.

logging.debug(f"processed {done} of {total}")

Common Mistakes That Make Logs Useless

A few failure modes turn a good idea into the same wall of text you were trying to escape.

Logging everything at INFO. If every step is INFO, INFO becomes noise. Reserve it for events you would want in a summary of the run.

Calling basicConfig() too late or twice. It only takes effect the first time, before any log calls. Put it at the top.

Logging inside a tight loop. Ten thousand near-identical lines hide the one that matters. Log the start, the end, and any anomaly — not every iteration.

Using logging.error() for non-errors. If everything is an error, the error level stops meaning anything. You will train yourself to ignore it.

Logging sensitive values. Passwords, tokens, and personal data in logs are hard to remove later, because logs get copied, archived, and shipped to other systems. Log the fact that a token was used, not the token.

When Logging Is the Wrong Tool

Logging is one instrument among several. Reaching for it everywhere is as much a mistake as never reaching for it.

  • A five-line script you will delete in an hour: print is faster. Use it.
  • Stepping through control flow line by line: a debugger is the right tool. Logging tells you what happened; a debugger tells you where you are standing.
  • Checking that a function returns the right value: a test is the right tool. A log line is not a pass or fail.
  • User-facing output: print or a proper interface is correct. Logs are for the person maintaining the script, not the person running it.

The one-line rule: logging is for events you want to review later, at a severity you chose in advance.

Practice: Add Logging to a Script You Already Wrote

Pick one script you have already written — a file cleanup, a report generator, anything with a few print calls in it.

  1. Replace three print statements with log calls at three different levels. A progress note becomes INFO, a missing-config fallback becomes WARNING, a failed write becomes ERROR.
  2. Add logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") at the top.
  3. Run the script twice: once with level=logging.INFO and once with level=logging.DEBUG. Note which lines appear and disappear.
  4. Success condition: you can change verbosity by editing one line, not by deleting code.

Optional extension: add %(asctime)s to the format if you have not already, and confirm the timestamp appears in the output.

Where to Go Next

A log message is a print statement with a severity and an off switch. That is the whole upgrade — and it is enough to make your next failure cheaper to isolate.

The natural next step is to write a small test for the same function you just added logging to. Logging tells you what happened during a run; a test tells you whether the function is correct before the run even starts. Together they turn debugging from archaeology into a quick check.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

You need to check whether a function returns the right value. Which tool does the article recommend?
Question 1 of 2Misconception Check

Focus: Select an appropriate tool for checking whether a function returns the right value.

A script uses an access token. What is the safer logging choice described in the article?
Question 2 of 2Single Choice

Focus: Avoid placing sensitive values in application logs.

References

  1. Logging HOWTO — Python 3.14.8 documentationdocs.python.org
Practical resource

Want a more structured Python path?

Use the Python Starter Pack to turn scattered tutorials into a focused practice path.

View the bundle
Coming soon

Python for Artificial Intelligence Starter Pack

Build a Python foundation you can actually use. The Python for AI Starter Pack brings together a guided path through setup, core programming concepts, data structures, files, JSON, APIs, debugging, and practical projects—so you can move quickly from running your first program to understanding and building useful software.

$9
PDF BundlePythonAIBeginner
  • 264-page illustrated PDF
  • 12 guided Python chapters
  • Visual concept diagrams
  • Self-assessment quizzes
  • Bonus deep-dive sections
  • Files, JSON, APIs, debugging & projects
  • Foundation for data, automation & AI

Coming soon

Free Python bundle

Get the LearnPyFast Python for Artificial Intelligence Starter Bundle

Build a Python foundation you can actually use. The Python for Artificial Intelligence Starter Pack brings together a guided path through setup, core programming concepts, data structures, files, JSON, APIs, debugging, and practical projects—so you can move quickly from running your first program to understanding and building useful software.

You’ll receive the bundle by email. You can unsubscribe anytime.

No spam. You can unsubscribe anytime. See our Privacy policy.

Related sites

Continue beyond Python

Explore related Worldmonger sites when you want to move from Python basics into JavaScript or LLM application building.

JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related tutorials

Continue with nearby Python topics and beginner-friendly explanations.