Logging B: Saving Logs to a File and Recording Important Actions

Lesson goals
  • Set up Logging that writes to a file and understand why the log “survives” between runs
  • Tell apart append mode and write (overwrite) mode
  • Record an Exception including the full Traceback with logging.exception
  • Decide which actions are important to record and at which level

What will we learn?

  • How to write logs to a file, and not only to the screen.
  • Writing modes: add to the end of the file, or start over.
  • How to record an error with all the details (Traceback).
  • What to record: which actions are important and at which level.

Why do we need this?

Remember the topic on the computer, the operating system and the network? What is on the screen and in memory disappears when the program ends. A file is kept on Storage. A log on the screen helps only whoever is sitting in front of it now. A log in a file can be opened tomorrow, searched, and used to understand what happened during the night.

On real servers, logs are the main tool for investigating faults. “Get me the logs from 3 a.m.” is a routine sentence in a development team.

Explanation from the basics

Logging to a file

You add the filename parameter to basicConfig. From that moment, the records are written to the file instead of to the screen. The file is created automatically if it does not exist, in the current folder (the Current Working Directory, like any relative path).

Writing modes (filemode)

ModeMeaningWhen
"a" (Append) — the defaultAdds to the end of the file. The previous logs are kept.Almost always. You want history.
"w" (Write)Deletes the previous content and starts over on every runIn practice, when you want only the last run

logging.exception — an error with all the details

logging.error("Payment failed") records only the message. Inside an except block, you can use logging.exception("..."): it records at the ERROR level and also attaches the full Traceback, so the file shows exactly on which line and in which function the error happened. As if you saw the crash yourself, without the program crashing.

What is worth recording?

Worth recordingWhy
The start and end of the program/serverTo know when it ran and when it stopped
Actions that change data: create, update, delete“Who deleted the order?”
Logging in and out, failed attemptsSecurity
Every error that was caught (except)Not to make problems disappear
Calls to external services and their result“Was the payment sent?”
Do not recordWhy
Passwords, credit card numbers, tokensSecurity and privacy
Every round of a big loop at INFO“Noise” that hides what matters. If at all, at DEBUG

Syntax

import logging

logging.basicConfig(
    filename="app.log",          # write to this file (relative to the working directory)
    filemode="a",                # "a" = append (default), "w" = overwrite each run
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    encoding="utf-8",            # safe for any characters in messages
)

try:
    risky()
except SomeError:
    logging.exception("What we were trying to do")   # ERROR + full traceback

First code example

week-2/file_logger.py:

import logging

logging.basicConfig(
    filename="app.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    encoding="utf-8",
)

logging.info("Program started")
logging.warning("Low disk space")
logging.info("Program finished")

print("Done. Check app.log")

On the screen:

Done. Check app.log

In the file app.log (after two runs):

2026-10-06 10:02:41,203 INFO     Program started
2026-10-06 10:02:41,203 WARNING  Low disk space
2026-10-06 10:02:41,204 INFO     Program finished
2026-10-06 10:03:15,881 INFO     Program started
2026-10-06 10:03:15,881 WARNING  Low disk space
2026-10-06 10:03:15,882 INFO     Program finished

Breaking down the code line by line

LineExplanation
filename="app.log"The logs are written to this file, not to the screen.
(no filemode)The default is "a": every run adds to the end of the file. That is why after two runs there are 6 lines.
encoding="utf-8"Allows writing any character, including non-English letters in messages (in our code the messages are in English, but usernames can be in any language).
print("Done...")print still reaches the screen. It is output for the user, and has nothing to do with the log.

To read the file: open it in VS Code, or from the Terminal with cat app.log. In PowerShell you can also follow it in real time: Get-Content app.log -Wait. On Mac/Linux: tail -f app.log.

Another example

A full log for a program that processes payments, with logging.exception. week-2/payments.py:

import logging

logging.basicConfig(
    filename="payments.log",
    filemode="w",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    encoding="utf-8",
)

def charge(customer, amount_text):
    amount = float(amount_text)
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    logging.info("Charged %s: %.2f NIS", customer, amount)
    return amount

payments = [("dana", "120"), ("avi", "abc"), ("noa", "-5"), ("yossi", "80.5")]

logging.info("Batch started: %s payments", len(payments))
total = 0
failed = 0

for customer, amount_text in payments:
    try:
        total = total + charge(customer, amount_text)
    except ValueError:
        failed = failed + 1
        logging.exception("Payment failed for %s (input: %r)", customer, amount_text)

logging.info("Batch finished: total %.2f NIS, %s failed", total, failed)
print("Batch done. Total:", total, "| Failed:", failed)

On the screen:

Batch done. Total: 200.5 | Failed: 2

In payments.log:

2026-10-06 10:20:01,010 INFO     Batch started: 4 payments
2026-10-06 10:20:01,010 INFO     Charged dana: 120.00 NIS
2026-10-06 10:20:01,011 ERROR    Payment failed for avi (input: 'abc')
Traceback (most recent call last):
  File "payments.py", line 28, in <module>
    total = total + charge(customer, amount_text)
  File "payments.py", line 13, in charge
    amount = float(amount_text)
ValueError: could not convert string to float: 'abc'
2026-10-06 10:20:01,012 ERROR    Payment failed for noa (input: '-5')
Traceback (most recent call last):
  File "payments.py", line 28, in <module>
    total = total + charge(customer, amount_text)
  File "payments.py", line 15, in charge
    raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -5.0
2026-10-06 10:20:01,012 INFO     Charged yossi: 80.50 NIS
2026-10-06 10:20:01,012 INFO     Batch finished: total 200.50 NIS, 2 failed

Breakdown:

  • filemode="w" — here we chose to overwrite on every run, so that in practice there is only the last log.
  • %.2f — a decimal number with 2 digits after the point (120.00). Suitable for money amounts.
  • %r — shows the value with quotes ('abc'). That way you clearly see what was received, including extra spaces or an empty string.
  • logging.exception(...) — an ERROR record, with the full Traceback below it. Both errors are ValueError, but for different reasons. The file shows exactly which line threw each one: float() (line 13) or our raise (line 15).
  • The program did not crash: it continues to the next payment, and all the information for investigating is saved.
  • print summarizes for the user in one line. The log tells the full story to the developer.

Common mistake

Mistake 1: “the log is not written / I can’t find the file”. The file is created in the Current Working Directory. If you ran from another folder, the file is there. (Remember the topic on the computer and the operating system!)

Mistake 2: logging.error inside except when you want to investigate.

except ValueError:
    logging.error("Payment failed")

It says it failed, but not why and not where. Inside except, logging.exception gives the full picture with no effort. (If the error is expected and ordinary, like wrong input from a user, and you do not need a Traceback, you can use logging.warning with the error’s message: logging.warning("bad input: %s", error).)

Mistake 3: filemode="w" on a real server. Every restart of the server deletes the history. Exactly after a crash, when you need the log most, it is deleted. On servers: only "a".

Mistake 4: thinking that logging.exception handles the error. It only records it. The decision what to do (continue, try again, pass it on with raise) is still yours.

Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

Exercise 1: A log to a file

Set up a log that is saved to the file shop.log, at level INFO, with time, level and message, and that is not deleted between runs. Record in it:

  • INFO when order number 17 is created;
  • WARNING when only 2 units are left in stock.
Show solution
import logging

logging.basicConfig(
    filename="shop.log",
    filemode="a",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    encoding="utf-8",
)

logging.info("order 17 created")
logging.warning("low stock: 2 left")
  • filename — the log is written to a file instead of to the screen.
  • filemode="a" — append: every run adds to the end of the file, and the history is kept.
  • The content of the file will look like this:

2026-11-01 10:00:01 INFO order 17 created 2026-11-01 10:00:01 WARNING low stock: 2 left

Exercise 2: append versus write

  1. What is the difference between filemode="a" and filemode="w"?
  2. A server crashed during the night and was restarted automatically. The log was set up with "w". What is the problem?
Show solution
  1. "a" (append) — adds to the end of the existing file. "w" (write) — deletes the file and starts over on every run.
  2. With "w", the restart deleted the log — exactly the information needed to understand why the server crashed. For logs you almost always use "a".

Exercise 3: logging.exception

Write a loop that charges a list of payments. Each amount is text; an amount that is not a number is recorded in the log with all the error’s details, and the loop continues. At the end the total amount is printed.

payments = ["120", "abc", "80"]

Expected output:

Total charged: 200
Show solution
import logging

logging.basicConfig(filename="payments.log", filemode="a", level=logging.INFO, encoding="utf-8")

payments = ["120", "abc", "80"]
total = 0
for amount_text in payments:
    try:
        total = total + int(amount_text)
        logging.info("charged %s", amount_text)
    except ValueError:
        logging.exception("invalid amount: %s", amount_text)

print("Total charged:", total)
Total charged: 200

logging.exception is called inside except. It records at the ERROR level, and adds the full Traceback to the log — that way you can see exactly where and why the amount failed.

Exercise 4: What to record and what not

For each item — record it in the log or not? Explain.

  1. The number of the order that failed.
  2. The password the user typed, when the login failed.
  3. The username that tried to log in.
  4. The full credit card number.
  5. How long a slow action took.
Show solution
  1. Record — without it you cannot investigate the fault.
  2. Do not record — passwords must never appear in the log. Anyone who can read the log would see them.
  3. Record — important for security: who tried to log in, and when.
  4. Do not record — sensitive information. At most the last four digits.
  5. Record — it helps find performance problems.

The rule: enough details to investigate — but never sensitive information.

Exercise 5: A login journal

You have a list of login attempts. Each attempt is a username and a password (two parallel lists). The correct password for all the users in the exercise is "1234". Record in the log security.log:

  • INFO for every successful login;
  • WARNING for every wrong password (without recording the password!).

At the end, print how many logins succeeded and how many failed.

users = ["dana", "avi", "dana", "noa"]
passwords = ["1234", "0000", "1111", "1234"]

Expected output:

Successful: 2, failed: 2
Show solution
import logging

CORRECT_PASSWORD = "1234"

logging.basicConfig(
    filename="security.log",
    filemode="a",
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    encoding="utf-8",
)

users = ["dana", "avi", "dana", "noa"]
passwords = ["1234", "0000", "1111", "1234"]

successful = 0
failed = 0
for i in range(len(users)):
    if passwords[i] == CORRECT_PASSWORD:
        successful = successful + 1
        logging.info("login ok: %s", users[i])
    else:
        failed = failed + 1
        logging.warning("wrong password for %s", users[i])

logging.info("summary: %s successful, %s failed", successful, failed)
print("Successful: " + str(successful) + ", failed: " + str(failed))
Successful: 2, failed: 2

Notice: the WARNING records the username — not the password.

Summary

  • basicConfig(filename="app.log") writes logs to a file on Storage, and they are kept after the program ends.
  • filemode="a" (the default) appends. "w" overwrites. On servers, always append.
  • logging.exception("...") inside except records an ERROR with the full Traceback, without the program crashing.
  • Record start/end, changes to data, security and errors. Do not record passwords and sensitive information.
  • The file is created in the Current Working Directory. tail -f / Get-Content -Wait follow it in real time.

Official documentation