The atexit module registers functions to run when your program exits normally. Essential for cleanup tasks.

Basic Registration

import atexit
 
def cleanup():
    print("Cleaning up...")
 
atexit.register(cleanup)
 
# Program runs...
# At exit: "Cleaning up..."

Register with Arguments

import atexit
 
def save_data(filename, data):
    print(f"Saving to {filename}")
    with open(filename, 'w') as f:
        f.write(str(data))
 
# Register with args
atexit.register(save_data, 'backup.txt', {'key': 'value'})
 
# Or use decorator syntax for no-arg functions
@atexit.register
def goodbye():
    print("Goodbye!")

Execution Order

Handlers run in reverse registration order (LIFO):

import atexit
 
atexit.register(print, "First registered, last executed")
atexit.register(print, "Second registered, second executed")
atexit.register(print, "Third registered, first executed")
 
# At exit:
# Third registered, first executed
# Second registered, second executed
# First registered, last executed

Unregister Handlers

import atexit
 
def cleanup():
    print("Cleaning up...")
 
atexit.register(cleanup)
 
# Later, if cleanup no longer needed:
atexit.unregister(cleanup)

When atexit Runs

Runs on:

  • Normal program termination
  • sys.exit() calls
  • Unhandled exceptions reaching top level

Doesn't run on:

  • os._exit() (immediate exit)
  • SIGKILL signal
  • Fatal Python errors
  • os.fork() in child process (unless registered after fork)

Practical Examples

Temporary File Cleanup

import atexit
import tempfile
import os
 
temp_files = []
 
def create_temp_file():
    fd, path = tempfile.mkstemp()
    temp_files.append(path)
    return path
 
def cleanup_temp_files():
    for path in temp_files:
        try:
            os.unlink(path)
        except OSError:
            pass
 
atexit.register(cleanup_temp_files)

Database Connection

import atexit
 
class Database:
    def __init__(self, connection_string):
        self.conn = connect(connection_string)
        atexit.register(self.close)
    
    def close(self):
        if self.conn:
            self.conn.close()
            self.conn = None
 
db = Database("localhost:5432")
# Connection automatically closed at exit

Logging Session End

import atexit
import logging
from datetime import datetime
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
start_time = datetime.now()
 
def log_session_end():
    duration = datetime.now() - start_time
    logger.info(f"Session ended. Duration: {duration}")
 
atexit.register(log_session_end)
logger.info("Session started")

Save Application State

import atexit
import json
 
class AppState:
    def __init__(self, state_file='state.json'):
        self.state_file = state_file
        self.data = self._load()
        atexit.register(self._save)
    
    def _load(self):
        try:
            with open(self.state_file) as f:
                return json.load(f)
        except FileNotFoundError:
            return {}
    
    def _save(self):
        with open(self.state_file, 'w') as f:
            json.dump(self.data, f)
    
    def set(self, key, value):
        self.data[key] = value
 
state = AppState()
state.set('last_run', str(datetime.now()))
# Automatically saved at exit

PID File Management

import atexit
import os
 
PID_FILE = '/var/run/myapp.pid'
 
def write_pid():
    with open(PID_FILE, 'w') as f:
        f.write(str(os.getpid()))
 
def remove_pid():
    try:
        os.unlink(PID_FILE)
    except OSError:
        pass
 
write_pid()
atexit.register(remove_pid)

Combining with Signal Handling

import atexit
import signal
import sys
 
cleanup_done = False
 
def cleanup():
    global cleanup_done
    if cleanup_done:
        return
    cleanup_done = True
    print("Cleanup...")
 
def signal_handler(signum, frame):
    cleanup()
    sys.exit(0)
 
atexit.register(cleanup)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

Context Manager Alternative

For scoped cleanup, prefer context managers:

# atexit: program-level cleanup
import atexit
atexit.register(global_cleanup)
 
# Context manager: scoped cleanup
from contextlib import contextmanager
 
@contextmanager
def managed_resource():
    resource = acquire()
    try:
        yield resource
    finally:
        resource.release()
 
with managed_resource() as r:
    use(r)
# Cleaned up here, not at program exit

Quick Reference

import atexit
 
# Register function
atexit.register(func)
atexit.register(func, *args, **kwargs)
 
# Decorator (no args)
@atexit.register
def cleanup():
    pass
 
# Unregister
atexit.unregister(func)
ScenarioSolution
Cleanup on exitatexit.register()
Scoped cleanupContext manager
Handle signalssignal + atexit
Force immediate exitos._exit() (skips atexit)

atexit is your last chance to run code. Use it for cleanup that must happen regardless of how the program ends.

React to this post: