Why this matters
When a Python program crashes, it prints a traceback. A traceback is written for the developer. It shows the full path to every source file, the line of code that failed, the names of your functions and variables, and sometimes the values they held. That is exactly the information a developer needs and exactly the information a user should never see.
For an ordinary user, a traceback is confusing and alarming. For someone probing your program, it is a map: where the files live, what libraries you use, how the code is organized, and which inputs make it fail. Error messages that reveal too much also leak private data. A message like No account for user alice@example.com tells anyone who asks which email addresses are registered.
Security here means two things at once. The user gets a message they can act on, and the program’s internals stay internal.
The rule: Messages shown to the user describe the problem in the user’s terms and say what to do next. Technical details go to a log the developer can read. The program never shows a raw traceback to the user, and never confirms or denies private facts through its error messages.
Benefits
- Users understand what happened and what to try, so they stop and ask for help less often.
- File paths, library names, and code structure stay out of the user’s view.
- The developer still gets full details, in a log, when something needs fixing.
- Error messages cannot be used to probe for which usernames, files, or records exist.
Red flags (violations)
- The program ends with a traceback on any error a user can cause.
print(e)orprint(f"Error: {e}")whereeis an exception the user did not need to understand.- Messages that include file system paths, such as
Could not open /Users/alice/project/data/inventory.txt. - Different messages for “wrong password” and “no such user.”
except Exception as e: print(traceback.format_exc())in user-facing code.
Example 1 - The raw traceback
Problematic code
def load_inventory(filename: str) -> dict:
items = {}
with open(filename) as f:
for line in f:
name, quantity = line.strip().split(",")
items[name] = int(quantity)
return items
def main():
items = load_inventory("data/inventory.txt")
...
If a line in the file is widgets,lots, the user sees:
Traceback (most recent call last):
File "/Users/alice/seng201/inventory/main.py", line 42, in <module>
main()
File "/Users/alice/seng201/inventory/main.py", line 31, in main
items = load_inventory("data/inventory.txt")
File "/Users/alice/seng201/inventory/storage.py", line 12, in load_inventory
items[name] = int(quantity)
ValueError: invalid literal for int() with base 10: 'lots'
The user now knows Alice’s username, the project layout, the module names, and that the quantity column is parsed with int().
Fixed with a user message and a developer log
import logging
logger = logging.getLogger(__name__)
def load_inventory(filename: str) -> dict:
"""Read the inventory file. Raises ValueError on a malformed line."""
items = {}
with open(filename) as f:
for line_number, line in enumerate(f, start=1):
try:
name, quantity = line.strip().split(",")
items[name] = int(quantity)
except ValueError as e:
raise ValueError(f"line {line_number} is malformed") from e
return items
def main():
logging.basicConfig(filename="inventory.log", level=logging.INFO)
try:
items = load_inventory("data/inventory.txt")
except FileNotFoundError:
logger.exception("Inventory file missing")
print("No saved inventory was found. Starting with an empty list.")
items = {}
except ValueError as e:
logger.exception("Inventory file could not be read")
print(f"The saved inventory file is damaged ({e}).")
print("Fix the file or delete it to start fresh.")
return
...
Why this is better: The user sees one sentence about what happened and one about what to do. logger.exception() writes the full traceback to inventory.log, where the developer can find it. The path, module names, and parsing details never reach the screen.
Example 2 - Confirming what exists
Problematic code
def remove_item(items: dict) -> None:
name = input("Item name: ").strip()
if name not in items:
print(f"Error: '{name}' not found in inventory dict (keys: {list(items)})")
return
del items[name]
The error message prints every item in the inventory. In a bigger system, the same habit prints every username, every account number, or every file in a folder.
Fixed by saying only what the user needs
def remove_item(items: dict) -> None:
name = input("Item name: ").strip()
if name not in items:
print(f"There is no item named '{name}'. Choose 3 to list the items you can remove.")
return
del items[name]
Why this is better: The message tells the user the name was not found and where to look. Listing the inventory is a separate feature the user chooses on purpose, with whatever access checks that feature has. The error path does not become a shortcut around them.
How to apply this rule
- Write messages for the user. Say what happened and what to do next, in words the user would use. “The file is damaged” instead of
ValueError: invalid literal. - Log the details. Use the
loggingmodule.logger.exception()inside anexceptblock records the full traceback to a file. - Catch at the top. The function that talks to the user is the only place that should turn an exception into a message. Lower-level functions raise.
- Never print paths or internals. If the message would help someone understand how your program is built, it belongs in the log.
- Use one message for one situation. When the difference between two failures would reveal private facts, give them the same message.
Knowledge Check
- What is wrong with printing
f"Error: {e}"to the user wheneis aFileNotFoundError?- Nothing; the user should see the real error
- The message includes the full file path and a Python error name, which helps nobody but a developer
FileNotFoundErrorcannot be converted to a string- The program will crash
Answer
2 — the string form of the exception includes the path the program tried to open. The user needs "no saved file was found," and the developer needs the path in the log. - Where should the full traceback go when a user-caused error occurs?
- To the screen, so the user can send it to you
- Nowhere; discard it
- To a log file, via
logger.exception() - To a comment in the code
Answer
3 — the log keeps the details for the developer without exposing them to the user. - A login function prints “No such user” for an unknown name and “Wrong password” for a known one. Why is this a leak?
- It is not a leak
- The two messages let anyone discover which usernames exist
- Passwords should never be checked
- The messages are too short
Answer
2 — by trying names and watching which message comes back, someone can build a list of valid accounts. One message, "Username or password is incorrect," closes that door. - Which message follows this principle?
Could not open /home/sam/app/data/inventory.txtKeyError: 'widgets'The saved inventory could not be read. Fix the file or delete it to start fresh.Traceback (most recent call last): ...
Answer
3 — it describes the problem in plain words and tells the user what to do. The others expose a path, a Python internal, or the whole stack.
Next up
Error handling covers the problems you expected. The next principle covers the ones you did not: cover all cases.