← 输入框|python.land|刷新

Python Try Except: Examples And Best Practices

Errors happen in every program: a file is missing, a user types “ten” instead of 10, a network connection drops. Python’s try and except keywords let you catch those errors and decide what happens next, instead of letting your program crash. This page starts with a short answer and then goes through everything else you need: catching specific and multiple exceptions, else and finally, raising your own exceptions, custom exceptions, exception groups, and the best practices that keep your error handling from hiding bugs.

What are try and except in Python?

try and except are Python’s way of handling errors. You put code that might fail in a try block. If an error (an exception) happens there, Python stops running that block and jumps to the matching except block, where you deal with the problem. If nothing goes wrong, the except block is skipped.

Here’s a typical example. Converting text to a number fails when the text isn’t a number, so we catch the ValueError that int() raises:

text = "42a"

try:
    number = int(text)
    print(f"You entered {number}")
except ValueError:
    print(f"{text!r} is not a whole number")Code language: Python (python)

Output:

'42a' is not a whole number

Change text to "42" and you’ll see You entered 42 instead. Without the try and except, the same bad input stops the whole program with a traceback:

Traceback (most recent call last): File “example.py”, line 2, in number = int(text) ValueError: invalid literal for int() with base 10: ’42a’

The last line of a traceback tells you which exception happened (ValueError), and that’s the name you put after except. If error messages like this still feel intimidating, the free lesson Errors Are Part of Programming in my Python Fundamentals course lets you cause one on purpose and fix it, right in your browser.

Try catch in Python? It’s called try except

If you come from Java, JavaScript, C# or PHP, you’re probably looking for try ... catch. Python has no catch keyword. It uses except instead, and it works the same way. Here’s how the keywords map:

Java / JavaScript / C#Python
try { ... }try:
catch (IOException e) { ... }except OSError as e:
finally { ... }finally:
throw new Exception("...")raise Exception("...")
(no equivalent)else: runs only if no exception happened

So a Java-style try catch looks like this in Python:

try:
    print(2/0)
except ZeroDivisionError as e:
    print("Caught:", e)
finally:
    print("This always runs")Code language: Python (python)
Caught: division by zero
This always runs

A few other differences are worth knowing. Python has no checked exceptions, so you never declare which exceptions a function throws. Raising an exception is called raising, not throwing (though people use both words). And exceptions are cheap enough in Python that using them for normal control flow is common and accepted. More on that in It’s better to ask for forgiveness.

What is an exception?

An exception is a condition that arises during a program’s execution. It is a signal that something unexpected happened. Python represents exceptions by an object of a specific type.

In Python, all built-in, non-system-exiting exceptions are derived from the Exception class. Exceptions have their own descriptive names. For example, if you try to divide a number by zero, you will get a ZeroDivisionError exception, which is also a subclass of the Exception class.

For a complete hierarchy of all exceptions, you can view the Python manual if you’re interested. Here’s a small excerpt from this hierarchy to illustrate:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- BaseExceptionGroup
 +-- Exception
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- OSError
      |    +-- FileNotFoundError
      |    +-- PermissionError
      .....

This hierarchy matters when you catch exceptions: except ArithmeticError also catches a ZeroDivisionError, and except OSError also catches a FileNotFoundError. If your knowledge about objects, classes, and inheritance is a bit rusty, you may want to read my article on objects and classes and my article on inheritance first.

Python try except

When something unexpected occurs, we can raise an exception at the point of the error. When an exception is raised, Python stops the current flow of execution and starts looking for an exception handler that can handle it. So what is an exception handler? Here’s where the try and except statements come into play.

python try except
try.. except.. else.. finally

As the illustration shows, we start a code block with a try statement. This means: try to run this code, but an exception might occur.

After our try block, one or more except blocks follow (or a finally block, more on that later). These except blocks can catch an exception, as we usually call this. Each except block can handle a specific type of exception.

Remember: classes are hierarchical. For that reason, exceptions are hierarchical as well. Python checks the except blocks from top to bottom and runs the first one that matches, so they must go from the most specific, like a ZeroDivisionError, to less specific, like an ArithmeticError. Get the order wrong and the specific block never runs:

try:
    print(2/0)
except ArithmeticError:
    print("Some arithmetic went wrong")
except ZeroDivisionError:
    print("You can't divide by zero!")Code language: Python (python)
Some arithmetic went wrong

The same thing happens, on a larger scale, if you start with an except block that catches Exception. That first block catches almost everything, so the other except blocks are useless.

When an exception is raised, the handler that catches it can be nearby, but it can also be in a different function or even a different module. Python doesn’t scan your code randomly for an exception handler. Instead, the handler should be somewhere in the call stack.

Call stack

A call stack is an ordered list of functions that are currently being executed. For example, you might call function A, which calls function B, which calls function C. We now have a call stack consisting of A, B, and C. When C raises an exception, Python will look for an exception handler in this call stack, going backward from end to start. It can be in function C (closest to the exception), in function B (somewhat farther), in function A, or even at the top level of the program where we called function A.

If Python finds a suitable except block, it executes the code in that block. If it doesn’t find one, Python handles the exception by itself. This means it will print the exception and exit the program since it has no clue what to do with it.

If this feels abstract, don’t worry. The examples below will make it concrete, and you might want to come back to this section after finishing the article.

Catching exceptions with try except

To handle an exception, we need to catch it. As we just learned, we catch an exception with the try and except keywords. When an exception occurs while we are inside the try block, the code in the matching except block is executed.

A simple try except example

Let’s try a simple example first. As you hopefully know, we can’t divide by the number zero. If we do so anyway, Python raises an exception called ZeroDivisionError, which is a subclass of ArithmeticError:

try:
    print(2/0)
except ZeroDivisionError:
    print("You can't divide by zero!")Code language: Python (python)
You can't divide by zero!

If you call a Python function inside the try block, and an exception occurs in that function, the flow of code execution stops at the point of the exception and the code in the except block is executed. Try doing this again without try and except, and you’ll see that Python prints the exception for us:

print(2/0)Code language: Python (python)

Output:

Traceback (most recent call last):
  File "divide_by_zero.py", line 1, in <module>
    print(2/0)
          ~^~
ZeroDivisionError: division by zero

Also, note that Python prints the error to stderr if you don’t handle the exception yourself.

Python try except with a specific exception

Always name the exception you expect after except. That way you handle the one problem you planned for, and anything else still shows up as a normal error. Here we read an optional setting from a dictionary and fall back to a default when the key is missing:

config = {"host": "localhost"}

try:
    port = config["port"]
except KeyError:
    port = 8080

print(f"Connecting to port {port}")Code language: Python (python)
Connecting to port 8080

Only a KeyError is caught. If you made a typo and wrote confg["port"], you’d get a NameError with a clear traceback, which is exactly what you want while developing. (For this particular case, config.get("port", 8080) from the dictionaries article is shorter, but the pattern is the same for any operation that can fail.)

Catching IOError (OSError and FileNotFoundError)

Let’s try another, more common example. After all, who divides a number by zero, right?

Exceptions are likely to occur when interacting with the outside world, e.g., when working with files or networks. For example, if you try to open a file with Python, but that file doesn’t exist, you get a FileNotFoundError. If you don’t have access to a file due to permissions, you get a PermissionError. Both are subclasses of OSError.

You’ll still see IOError in older tutorials. Since Python 3.3 it’s just another name for OSError, so except IOError still works, but OSError (or the more specific subclasses) is what you should write today.

Assignment

Please do the following:

  1. Run the code below on your computer, and notice the file name (it doesn’t exist). See what happens.
  2. Create a file called myfile.txt in the same folder, change the file name in the code to myfile.txt, and rerun the code. What happens now?

Here’s the code to copy/paste:

try:
    # Open file in read-only mode
    with open("not_here.txt", 'r') as f:
        f.write("Hello World!")
except OSError as e:
    print("An error occurred:", e)Code language: Python (python)

Answers

The file is not found in the first case. You should get this output:

An error occurred: [Errno 2] No such file or directory: 'not_here.txt'

You’ll still get an error after creating the file in the second case. This time, we’re trying to write to a file that is opened in read-only mode. For more information on these modes, read the article on opening, reading, and writing files with Python. The error should look like this:

An error occurred: not writable

Although this is an error, it’s not written to the stderr output of the operating system. That’s because we handled the exception ourselves. If you remove the try and except from the code and then try to write to the file in read-only mode, the exception reaches the top of the program, Python stops the program, and shows this message:

Traceback (most recent call last): File “tryexcept.py”, line 2, in f.write(“Hello World!”) ~~~~~~~^^^^^^^^^^^^^^^^ io.UnsupportedOperation: not writable

Catching multiple exceptions in Python

Often more than one thing can go wrong in the same piece of code. You have two options, depending on whether you want to handle the problems the same way or differently.

Catch multiple exceptions in one except block

If you want the same response to several exceptions, put them in parentheses as a tuple:

def parse_age(value):
    try:
        return int(value)
    except (ValueError, TypeError):
        return None

print(parse_age("39"))
print(parse_age("thirty-nine"))
print(parse_age(None))Code language: Python (python)
39
None
None

int("thirty-nine") raises a ValueError, while int(None) raises a TypeError. One except block handles both.

Since Python 3.14, you may leave out the parentheses: except ValueError, TypeError: works too. The parentheses are still required when you add as e, and older Python versions need them in all cases, so I’d keep writing them.

Use multiple except blocks

If each problem needs its own response, write one except block per exception:

def read_number(path):
    try:
        with open(path) as f:
            return int(f.read())
    except FileNotFoundError:
        print(f"{path} doesn't exist")
    except ValueError:
        print(f"{path} doesn't contain a number")

read_number("missing.txt")Code language: Python (python)
missing.txt doesn't exist

Python runs at most one except block: the first one that matches. Remember to put specific exceptions above more general ones.

The exception object: except Exception as e

Add as and a name to an except clause to get hold of the exception object itself. By convention, that name is e. The object tells you what went wrong:

try:
    with open("settings.toml") as f:
        settings = f.read()
except FileNotFoundError as e:
    print("Message:  ", e)
    print("Type:     ", type(e).__name__)
    print("Filename: ", e.filename)
    print("Args:     ", e.args)Code language: Python (python)
Message:   [Errno 2] No such file or directory: 'settings.toml'
Type:      FileNotFoundError
Filename:  settings.toml
Args:      (2, 'No such file or directory')

Printing e (or calling str(e)) gives you the error message. type(e).__name__ gives you the exception’s class name, which is handy when you catch several types in one block. Every exception has args, the values it was created with, and some have extra attributes, like filename on OSError and its subclasses.

One thing that surprises people: e only exists inside the except block. Python deletes it when the block ends:

try:
    1 / 0
except ZeroDivisionError as e:
    pass

print(e)Code language: Python (python)
Traceback (most recent call last): File “example.py”, line 6, in print(e) ^ NameError: name ‘e’ is not defined

If you need the exception later, assign it to another variable inside the except block.

How to print a Python exception

You can print exceptions directly as long as you catch them with as e. You’ve seen this above already, but to be clear, here’s the basic pattern:

try:
    ...
except Exception as e:
    print("There was an error:", e)Code language: Python (python)

If you’d like to print the full traceback, just like Python does when you don’t catch the exception yourself, you can import the traceback module. The program keeps running afterward:

import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()

print("The program keeps running")Code language: Python (python)
Traceback (most recent call last): File “example.py”, line 4, in 1 / 0 ~~^~~ ZeroDivisionError: division by zero The program keeps running

In larger programs you’ll usually log errors instead of printing them. Inside an except block, logging.exception("Something failed") writes your message plus the full traceback to the log.

Python try except else and finally

Remember the illustration at the start of this article? Besides try and except, it has two more blocks: else and finally. Here’s the whole thing in one example:

def show_number(text):
    try:
        number = int(text)
    except ValueError:
        print(f"{text!r} is not a number")
    else:
        print(f"{number} squared is {number ** 2}")
    finally:
        print("Done with", repr(text))

show_number("12")
show_number("twelve")Code language: Python (python)
12 squared is 144
Done with '12'
'twelve' is not a number
Done with 'twelve'

The else block ran only for "12", where no exception happened. The finally block ran both times. Let’s look at each in more detail.

The finally block in try-except

The finally block is executed regardless of whether an exception occurs or not. It even runs when the try block hits a return, or when an exception isn’t caught at all. That makes finally blocks useful when you want to close a file or a network connection no matter what happens, so you don’t leak resources.

Here’s an example in which we open a file without using the with statement, forcing us to close it ourselves:

def read_file(path):
    f = open(path)
    try:
        return f.read()
    finally:
        print("Closing the file now")
        f.close()

with open("myfile.txt", "w") as f:
    f.write("Hello World!")

print(read_file("myfile.txt"))Code language: Python (python)
Closing the file now
Hello World!

Notice that open() is outside the try block. If the file doesn’t exist, open() fails, there’s nothing to close, and the FileNotFoundError goes to the caller. If you put open() inside the try block, a missing file would make f.close() in the finally block fail with a NameError, because f was never assigned.

In practice, the with statement does this cleanup for you, and it’s what you should use for files. finally is for the cases where no context manager exists.

One warning: don’t put return, break or continue inside a finally block. They silently throw away any exception that was on its way out. Since Python 3.14, doing so gives a SyntaxWarning.

The else block in try-except

In addition to the except and finally blocks, you can add an else block. The else block executes only when no exception occurs in the try block. So it differs from the finally block, since finally executes even if an exception occurs.

When should you use the else block? And why shouldn’t you just add extra code to the try block? Good questions!

According to the Python manual, using the else clause is better than adding additional code to the try clause. The reasoning is that it avoids accidentally catching an exception that wasn’t raised by the code you meant to protect. In the show_number example above, only int(text) is inside the try block. If the print line in the else block somehow raised a ValueError, it would not be mistaken for bad input.

I admit I don’t use else blocks very often myself, and they can be confusing for people coming from other languages, which have nothing like it. A common alternative is to return from the try block or put the follow-up code after the whole try statement, as long as the try block itself stays small.

Raising (or throwing) exceptions

So far we’ve caught exceptions that Python raised. You can also raise an exception yourself with the raise keyword. Do this when your function gets something it can’t work with. Raising is better than returning a special value like None or -1, because the caller can’t accidentally ignore it.

def set_age(age):
    if age < 0:
        raise ValueError(f"age can't be negative, got {age}")
    print(f"Age set to {age}")

set_age(39)
set_age(-1)Code language: Python (python)
Age set to 39 Traceback (most recent call last): File “example.py”, line 7, in set_age(-1) ~~~~~~~^^^^ File “example.py”, line 3, in set_age raise ValueError(f”age can’t be negative, got {age}”) ValueError: age can’t be negative, got -1

Pick the built-in exception that fits: ValueError for a value of the right type that’s still wrong, TypeError for the wrong type, KeyError or LookupError for something that can’t be found. And always include a message that says what was wrong.

Re-raise an exception

Sometimes you want to do something when an exception happens, like logging it, but you can’t actually fix the problem. In that case, use raise on its own inside the except block. It re-raises the same exception with its original traceback:

def load(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        print(f"Logging: {path} is missing")
        raise

load("config.toml")Code language: Python (python)
Logging: config.toml is missing Traceback (most recent call last): File “example.py”, line 9, in load(“config.toml”) ~~~~^^^^^^^^^^^^^^^ File “example.py”, line 3, in load with open(path) as f: ~~~~^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: ‘config.toml’

Raise from: chaining exceptions

Often you’ll want to turn a low-level error into one that makes sense to the caller. A missing dictionary key or a failed int() means little to someone who just wants to load a config. Use raise ... from e to raise a new exception while keeping the original one attached:

class ConfigError(Exception):
    pass

def read_port(settings):
    try:
        return int(settings["port"])
    except (KeyError, ValueError) as e:
        raise ConfigError("port must be set to a number") from e

read_port({"port": "eighty"})Code language: Python (python)
Traceback (most recent call last): File “example.py”, line 6, in read_port return int(settings[“port”]) ValueError: invalid literal for int() with base 10: ‘eighty’ The above exception was the direct cause of the following exception: Traceback (most recent call last): File “example.py”, line 10, in read_port({“port”: “eighty”}) ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^ File “example.py”, line 8, in read_port raise ConfigError(“port must be set to a number”) from e ConfigError: port must be set to a number

The traceback shows both exceptions: first the cause, then the new one. The caller only has to catch ConfigError, and whoever debugs it still sees the real reason. The original exception is also available as e.__cause__.

If the original exception is just noise, write raise ConfigError(...) from None. The traceback then shows only the new exception. Use this sparingly: hiding the cause makes debugging harder.

Add a note to an exception

Since Python 3.11, you can add extra context to an exception without replacing it, using add_note(). The note shows up below the error message:

rows = ["1", "2", "three"]

for line_number, row in enumerate(rows, start=1):
    try:
        value = int(row)
    except ValueError as e:
        e.add_note(f"while reading line {line_number} of data.csv")
        raiseCode language: Python (python)
Traceback (most recent call last): File “example.py”, line 5, in value = int(row) ValueError: invalid literal for int() with base 10: ‘three’ while reading line 3 of data.csv

Create custom exceptions

All built-in, non-system-exiting exceptions are derived from the Exception class as we learned before. All user-defined exceptions should also be derived from this class. So if we want to create our own exceptions, we need to create a subclass of the Exception class.

For example, if you want to create an exception that indicates that a user was not found, you can create a UserNotFoundError exception. This would, in its most basic form, look like this:

class UserNotFoundError(Exception):
    passCode language: Python (python)

This inherits all the properties and methods of Exception, but we give it a new name to distinguish it from the Exception class. This way, we’ll be able to specifically catch it with an except block. By convention, exception names end in Error.

The name of this exception clearly tells us the type of problem that was encountered, so as an added bonus, it works as code documentation as well. Just like well-named variables and functions, a well-named exception makes a big difference when reading back your code.

In the example below, we call a function, fetch_user, that fetches some user data from an imaginary database. If the user is not found, this database returns None. We decided that we don’t want to return None, which would force the caller to check for None every time. Instead, we raise our custom UserNotFoundError:

class UserNotFoundError(Exception):
    pass

def fetch_user(user_id):
    # Here you would fetch from some kind of db, e.g.:
    # user = db.get_user(user_id)

    # To make this example runnable, let's set it to None
    user = None

    if user is None:
        raise UserNotFoundError(f"User {user_id} not in database")
    return user

users = [123, 456, 789]
for user_id in users:
    try:
        fetch_user(user_id)
    except UserNotFoundError as e:
        print("There was an error:", e)Code language: Python (python)
There was an error: User 123 not in database
There was an error: User 456 not in database
There was an error: User 789 not in database

Assignment

Here’s a little assignment. We could have used a regular Exception object instead. That way, we don’t need to define a custom one. Why is this a bad idea?

Answer

You can in fact raise a regular exception, e.g. with raise Exception('User not found'). But if you do, you need to catch all exceptions of type Exception. And as we know, there are a lot of those. Chances are you inadvertently catch some other exception that you’re not able to handle. For example, the database client might throw a DatabaseAuthenticationError which is also a subclass of Exception.

Custom exceptions with extra data

Custom exceptions are ordinary classes, so you can give them attributes that the handler can use. In a bigger project, it also helps to define one base exception for your code and derive the others from it. Callers can then catch one specific error, or all of yours at once:

class ShopError(Exception):
    """Base class for all errors in our shop code."""

class OutOfStockError(ShopError):
    def __init__(self, product, requested, available):
        super().__init__(f"{product}: asked for {requested}, only {available} left")
        self.product = product
        self.available = available

class PaymentDeclinedError(ShopError):
    pass

try:
    raise OutOfStockError("coffee mug", 3, 1)
except OutOfStockError as e:
    print(e)
    print(f"We can still sell you {e.available}")
except ShopError as e:
    print("Something else went wrong in the shop:", e)Code language: Python (python)
coffee mug: asked for 3, only 1 left
We can still sell you 1

Calling super().__init__() with the message makes print(e) show something useful.

Exception groups and except* (Python 3.11+)

Normally, one exception happens at a time. But sometimes several independent things fail together: you validate a form and three fields are wrong, or you run five network requests at once and two of them fail. Python 3.11 added ExceptionGroup for this: one exception that carries a list of other exceptions.

To handle an exception group, use except* (pronounced “except star”). Each except* clause picks out the exceptions of its type from the group. Unlike a normal try statement, several except* clauses can run:

def validate(user):
    errors = []
    if not user.get("name"):
        errors.append(ValueError("name is required"))
    if not isinstance(user.get("age"), int):
        errors.append(TypeError("age must be a whole number"))
    if "email" not in user:
        errors.append(KeyError("email"))
    if errors:
        raise ExceptionGroup("invalid user", errors)

try:
    validate({"name": "", "age": "39"})
except* ValueError as eg:
    for e in eg.exceptions:
        print("Value problem:", e)
except* TypeError as eg:
    for e in eg.exceptions:
        print("Type problem:", e)
except* KeyError as eg:
    for e in eg.exceptions:
        print("Missing field:", e)Code language: Python (python)
Value problem: name is required
Type problem: age must be a whole number
Missing field: 'email'

Inside an except* block, the name after as is itself an exception group, holding only the matching exceptions. That’s why we loop over eg.exceptions.

Any exceptions in the group that no except* clause matches are raised again, still as a group:

try:
    raise ExceptionGroup("two problems", [ValueError("bad value"), OSError("disk full")])
except* ValueError as eg:
    print("Handled:", eg.exceptions)Code language: Python (python)
Handled: (ValueError(‘bad value’),) + Exception Group Traceback (most recent call last): | File “example.py”, line 2, in | raise ExceptionGroup(“two problems”, [ValueError(“bad value”), OSError(“disk full”)]) | ExceptionGroup: two problems (1 sub-exception) +-+—————- 1 —————- | OSError: disk full +————————————

You’ll meet exception groups most often in asyncio. When tasks in an asyncio.TaskGroup fail, their exceptions arrive together as a group:

import asyncio

async def fetch(name, fail):
    await asyncio.sleep(0.1)
    if fail:
        raise ConnectionError(f"{name} is down")
    return f"{name} ok"

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(fetch("api", fail=True))
            tg.create_task(fetch("db", fail=True))
            tg.create_task(fetch("cache", fail=False))
    except* ConnectionError as eg:
        for e in eg.exceptions:
            print("Failed:", e)

asyncio.run(main())Code language: Python (python)
Failed: api is down
Failed: db is down

If you’re just starting out, you can skip exception groups for now. Regular except is what you’ll write 99% of the time. You can’t mix except and except* in the same try statement.

Common Python exceptions

Some exceptions are so common that you’ll inevitably encounter them. Here are the ones you’ll see most:

Exception nameWhen you’ll encounter itExample that raises it
SyntaxErrorRaised when there is an error in Python syntax. Python finds it before running your code, so you can’t catch it in the same file.print('test)
NameErrorYou use a variable or function name that doesn’t exist, often a typo.pritn("hi")
TypeErrorAn operation gets a value of the wrong type."age: " + 39
ValueErrorThe type is right, but the value isn’t.int("ten")
KeyErrorRaised when a key is not found in a dictionary.d = {'a': 1}
d['b']
IndexErrorRaised when an index is out of range.lst = [1, 2, 3]
lst[10]
AttributeErrorAn object doesn’t have the attribute or method you asked for.None.upper()
ZeroDivisionErrorYou divide by zero.2 / 0
FileNotFoundErrorYou open a file that doesn’t exist.open("nope.txt")
KeyboardInterruptRaised when the user hits the interrupt key (Ctrl+C)Pressing control+c
Some common exceptions you will encounter at some point in your career

If you like, you can try to trigger these exceptions on purpose. I promise you that you will encounter these countless times in your Python programming career. Understanding what they mean and when they occur will greatly help you debug your code.

Python try except best practices

Now that we know the mechanics of handling exceptions, I’d like to share a couple of best practices with you. Most of them come down to one idea: handle the errors you expect and know how to deal with, and let everything else crash loudly.

Catch narrowly: specific exceptions, small try blocks

Catch the most specific exception you can, and only catch it if you have a real plan for it, such as a default value, a retry, or a clear message to the user. Also keep the try block small: wrap only the line that can fail, not the whole function. A big try block can catch an exception from a line you didn’t expect to fail, and then your handler does the wrong thing:

prices = {"apple": 0.5, "pear": 0.75}

def price_of(item):
    try:
        price = prices[item]
    except KeyError:
        return None
    return round(price * 1.21, 2)

print(price_of("apple"))
print(price_of("kiwi"))Code language: Python (python)
0.6
None

Only the dictionary lookup is inside the try block. A bug in the calculation below it will still show up as an error instead of quietly returning None.

Don’t silence errors

The worst exception handler is this one:

try:
    do_something()
except Exception:
    passCode language: Python (python)

Your program keeps running as if nothing happened, possibly with missing or wrong data, and you have no idea anything failed. When you do handle an error, leave a trace. Log it, show a message, or re-raise it:

import logging

logging.basicConfig(format="%(levelname)s: %(message)s")

def load_avatar(path):
    try:
        with open(path, "rb") as f:
            return f.read()
    except FileNotFoundError:
        logging.warning("No avatar at %s, using the default", path)
        return b""

load_avatar("avatars/erik.png")Code language: Python (python)
WARNING: No avatar at avatars/erik.png, using the default

Sometimes ignoring an exception really is correct, like deleting a file that may already be gone. Then say so explicitly with contextlib.suppress, and name the exact exception:

import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("old_cache.tmp")

print("Cache cleared (or there was nothing to clear)")Code language: Python (python)
Cache cleared (or there was nothing to clear)

Don’t use a bare except

I’ve written about this in the blog post ‘How not to handle exceptions in Python‘. Don’t use a blank (or “bare”) except when you want to catch a broad range of exceptions. By this, I mean something like:

try:
    ...
except:
    print("An error occurred")Code language: Python (python)

You might encounter this in code samples on the web. If you do, make a habit of improving the exception handling. Why should you, and how can you improve code like the example above?

All exceptions, including system exceptions, inherit from a class called BaseException. If an except clause mentions a particular class, that clause also handles any exception classes derived from that class. An empty except is equivalent to except BaseException, hence it will catch all possible exceptions.

So, although the syntax is allowed, I don’t recommend it. You’ll also catch KeyboardInterrupt and SystemExit, which means Ctrl+C and sys.exit() no longer stop your program. Instead, catch the explicit exceptions you can handle. Or, if you really need to, catch the Exception base class to handle almost all the regular exceptions, but not the system ones.

If you’re feeling adventurous, you can try to catch all exceptions and see what happens:

from time import sleep

while True:
    try:
        print("Try and stop me")
        sleep(1)
    except:
        print("Don't stop me now, I'm having such a good time!")

Pressing Ctrl+C won’t stop this program; you’ll probably need to close your terminal. Now change the except block to catch Exception. You will still catch almost all exceptions, but the program will exit on system exceptions like KeyboardInterrupt and SystemExit:

from time import sleep

while True:
    try:
        print("Try and stop me")
        sleep(1)
    except Exception:
        print("Something went wrong")

It’s better to ask for forgiveness

In Python, you’ll often see a pattern where people simply try if something works, and if it doesn’t, catch the exception. In other words, it’s better to ask for forgiveness than permission (EAFP). This is in contrast to other languages, where you preferably ask for permission. For example, in Java, exceptions can slow down your program, and you “ask for permission” by doing checks on an object instead of simply trying.

To make this more concrete: in Python, we often just try to access the key in a dictionary. If the key doesn’t exist, we’ll get an exception and handle it. Suppose we just converted some externally provided JSON to a dictionary and now start to use it:

import json

user_json = '{"name": "John", "age": 39}'
user = json.loads(user_json)

try:
    print(user['name'])
    print(user['age'])
    print(user['address'])
except KeyError as e:
    print("There are missing fields in the user object:", e)Code language: Python (python)

This prints:

John
39
There are missing fields in the user object: 'address'

We could have added three checks (if 'name' in user, if 'age' in user, etc.) to make sure that all the fields are there. But that adds a lot of code just to check if keys exist. Instead, we ask for forgiveness in our except block once, which is cleaner and more readable. And if you worry about performance: exceptions don’t take up that many CPU cycles in Python. Lots of comparisons are often slower than catching a single exception (if it occurs at all!).

Keep learning

Here are some more resources to deepen your knowledge:

Related articles