Difference Between `except:` and `except Exception as e:` in Python

The last time I had to debug a production incident caused by exception handling, the bug wasn’t in the business logic. The bug was in a single line meant to be “safe”: a bare except: that swallowed a shutdown signal, kept a worker alive in a half-broken state, and quietly skipped the error reporting we relied on. The service didn’t crash (which looked good on dashboards), but it also didn’t do its job (which was the actual goal).\n\nIf you’ve ever written Python that talks to a database, reads files, calls APIs, or runs scheduled jobs, you’re already living in the world where exceptions are normal control flow for abnormal situations. The key is being intentional about which exceptions you catch, which ones you let terminate the program, and what information you preserve for debugging.\n\nYou’ll walk away knowing exactly what except: catches, what except Exception as e: catches, why that difference matters for Ctrl+C and program exits, and the patterns I recommend in modern Python codebases.\n\n## Two Similar Lines With Very Different Blast Radius\nAt a glance, these two blocks look like stylistic variants:\n\npython\ntry:\n result = runreport()\nexcept:\n print(‘Something went wrong‘)\n\n\npython\ntry:\n result = runreport()\nexcept Exception as e:\n print(f‘Something went wrong: {e}‘)\n\n\nThey are not equivalent.\n\n- except: catches almost everything, including exceptions that are specifically designed to stop your program (or at least stop what it’s doing).\n- except Exception as e: catches the standard “runtime error” family of problems, while letting “stop now” signals through.\n\nThat single difference is why bare except: is a frequent source of flaky CLIs, unkillable scripts, stuck background jobs, and mysteriously missing stack traces.\n\nOne more framing that helps when you’re deciding what to write: these two handlers answer different questions.\n\n- except: answers: “No matter what just happened, keep going.”\n- except Exception as e: answers: “If a normal runtime error happened, handle it; otherwise, honor shutdown/cancellation.”\n\nIn production, that distinction is the difference between a service that exits cleanly when it’s told to stop and a service that ignores stop signals and gets force-killed later (often without flushing logs, finishing in-flight work, or releasing locks).\n\n## The Exception Family Tree: BaseException vs Exception\nPython’s exception hierarchy is the foundation for understanding what you’re catching.\n\n- BaseException is the root of (almost) all exceptions.\n- Exception is the base class for most application-level errors.\n\nA few important exceptions intentionally inherit directly from BaseException, not Exception. The most common ones you’ll run into:\n\n- KeyboardInterrupt: raised when a user presses Ctrl+C in a terminal.\n- SystemExit: raised by sys.exit() to request a clean program exit.\n- GeneratorExit: used internally when generators are closed.\n\nSo the split is deliberate:\n\n- Catching Exception usually means “handle errors, but don’t block program shutdown.”\n- Catching BaseException (or using bare except: which behaves like it) usually means “handle everything, including shutdown signals.”\n\nWhen I review code, I treat bare except: as a high-suspicion construct because it changes the shutdown behavior of the program.\n\nIf you like mental models, I think of it like this:\n\n- Exception is the “stuff went wrong in my program” bucket.\n- BaseException is the “my program is being told to stop / unwind / exit” bucket plus the normal error bucket.\n\nThat matters because “stop” signals are not just a developer pressing Ctrl+C. They show up as:\n\n- A container runtime sending SIGTERM during deployment rollouts.\n- A job scheduler cancelling a task due to timeout.\n- A service manager requesting a clean stop so logs flush and sockets close.\n\nYou generally want those to work even if your code is currently in an error-handling path.\n\n## What Bare except: Really Catches (Including Ctrl+C and sys.exit())\nA bare except: is effectively “catch BaseException” in most practical terms.\n\nHere’s a runnable demonstration that shows the surprising part: a bare except: can swallow a Ctrl+C-like interruption.\n\npython\ndef runbatchjob():\n # Imagine this is doing real work.\n raise KeyboardInterrupt(‘User requested stop (Ctrl+C)‘)\n\ntry:\n runbatchjob()\nexcept:\n print(‘Caught something and kept going‘)\n\nprint(‘Program still running‘)\n\n\nIf you run that, you’ll see that the program continues. In a CLI tool, that’s usually the opposite of what you want. Users press Ctrl+C because they want the process to stop.\n\nNow the sys.exit() case:\n\npython\nimport sys\n\ndef shutdown():\n sys.exit(2)\n\ntry:\n shutdown()\nexcept:\n print(‘Swallowed SystemExit, continuing anyway‘)\n\nprint(‘Exit code lost‘)\n\n\nSwallowing SystemExit breaks a lot of implicit contracts:\n\n- Shell scripts and CI jobs rely on exit codes.\n- Process supervisors rely on clean termination.\n- Containers rely on correct stop behavior.\n\nThe debugging cost is also real. Bare except: blocks commonly:\n\n- Hide the original stack trace.\n- Convert a crisp failure into vague behavior (like a default value, partial output, or silent skip).\n- Make it harder for error monitors to capture the exception (because you handled it and didn’t report it).\n\nAnd the weird part is that it often looks like it improved reliability. If you swallow exceptions, of course the process crashes less. But that’s reliability theater if the system is now quietly failing.\n\n### The silent-failure pattern (the one that burns weekends)\nThe most expensive bare-except: bugs I see are inside loops. Something like this:\n\npython\nfor item in items:\n try:\n process(item)\n except:\n pass\n\n\nIt’s tempting because it makes batch jobs “keep going” even if one record is bad. But it also creates a black hole where all kinds of failures disappear, including ones that should stop the job immediately (like corrupted environment, invalid credentials, database outages, and yes, shutdown signals).\n\nIf you truly need “keep going,” you want “keep going for specific errors” and you want visibility into how often it happens. I’ll show concrete patterns for that later.\n\n### Rare but real: GeneratorExit\nGeneratorExit is mostly an implementation detail, but it’s another reason bare except: is so broad. When generators are closed (explicitly or by garbage collection), Python uses GeneratorExit as part of the unwinding mechanism. Accidentally catching it can lead to generators that don’t close correctly or cleanup that doesn’t happen when you expect.\n\nThere are rare valid uses for bare except:, but they are usually at the very top of a process boundary (for example, a last-resort crash shield that logs and then re-raises or exits). If you’re not doing something that deliberate, it’s a code smell.\n\n## What except Exception as e: Catches (And Why the e Matters)\nexcept Exception as e: catches exceptions that inherit from Exception, and it gives you the exception instance in e.\n\nTwo things that changes immediately:\n\n1. Shutdown signals still work.\n2. You can record meaningful diagnostics.\n\nHere’s the same interruption example, but using Exception:\n\npython\ndef runbatchjob():\n raise KeyboardInterrupt(‘User requested stop (Ctrl+C)‘)\n\ntry:\n runbatchjob()\nexcept Exception as e:\n print(f‘Caught application error: {e}‘)\n\nprint(‘This line will not run‘)\n\n\nThis time, the KeyboardInterrupt is not caught (because it’s not an Exception), and the program stops as expected.\n\nNow, a realistic example: parsing a configuration file.\n\npython\nimport json\nfrom pathlib import Path\n\ndef loadconfig(path: str) -> dict:\n configtext = Path(path).readtext(encoding=‘utf-8‘)\n return json.loads(configtext)\n\ntry:\n config = loadconfig(‘config.json‘)\n print(‘Loaded config keys:‘, sorted(config.keys()))\nexcept Exception as e:\n # In production, I usually log this with stack info.\n print(f‘Failed to load config: {e!r}‘)\n raise # Preserve the original traceback\n\n\nA few details I care about in that pattern:\n\n- I print e!r (repr) when I need precision. str(e) is often human-friendly but can be ambiguous.\n- I re-raise with bare raise so the traceback points to the original line where things broke.\n- I still didn’t use bare except: because I don’t want to catch Ctrl+C.\n\nThe as e binding is not just for printing. It’s also how you:\n\n- Attach context with raise NewError(...) from e.\n- Branch on exception type when you’re catching a small group.\n- Feed error monitors and logs with structured data.\n\n### One subtle gotcha: don’t raise e\nIf you catch an exception and want to re-raise it, prefer raise (no variable).\n\npython\ntry:\n dothing()\nexcept Exception as e:\n logproblem(e)\n raise\n\n\nUsing raise e can replace the traceback context in ways that make debugging harder. The plain raise preserves the original traceback more faithfully.\n\n## Patterns I Ship: Specific First, Exception Second, Bare Almost Never\nIf you want one rule you can apply immediately: catch the most specific exceptions you can, and only broaden when you have a clear reason.\n\n### Pattern 1: Specific exceptions with a helpful message\nWhen you know what can go wrong, be explicit.\n\npython\nfrom decimal import Decimal, InvalidOperation\n\ndef parseprice(text: str) -> Decimal:\n # Accept input from a CSV or form field.\n cleaned = text.strip().replace(‘$‘, ‘‘)\n return Decimal(cleaned)\n\ntry:\n price = parseprice(‘$12.34‘)\n print(‘Parsed price:‘, price)\nexcept InvalidOperation as e:\n print(f‘Invalid price format: {e}‘)\n\n\nThis reads like documentation and makes error handling predictable.\n\nIf you’re handling user input, I usually go one step further: include enough context to reproduce the issue, but not so much that you leak secrets. For example, truncating and redacting is a habit worth having.\n\n### Pattern 2: Catch Exception at a boundary, then re-raise\nI often catch Exception at I/O boundaries (CLI entrypoints, worker loops, web request handlers) to log context, then re-raise.\n\npython\nimport logging\n\nlogger = logging.getLogger(‘billing‘)\n\ndef createinvoice(customerid: str) -> str:\n # Placeholder for real logic.\n raise RuntimeError(‘Database write failed‘)\n\ntry:\n invoiceid = createinvoice(‘cust20391‘)\n print(‘Created invoice:‘, invoiceid)\nexcept Exception as e:\n logger.exception(‘Invoice creation failed for customerid=%s‘, ‘cust20391‘)\n raise\n\n\nlogger.exception(...) records a stack trace automatically. That matters more than printing e.\n\nA practical refinement I use in real services: log the domain identifiers that let you find the request/customer/job in other systems (request id, job id, tenant id). Don’t log entire payloads by default.\n\n### Pattern 3: When you actually want Ctrl+C handling\nSometimes you do want to intercept Ctrl+C, but do it explicitly so your intent is obvious.\n\npython\ndef runcli():\n while True:\n # Do work...\n raise KeyboardInterrupt\n\ntry:\n runcli()\nexcept KeyboardInterrupt:\n print(‘\nStopped by user‘)\n\n\nNotice I still didn’t use bare except:. I named what I was handling.\n\n### Pattern 4: The narrow (and legitimate) use of bare except:\nIf you have cleanup code that must run no matter what, you usually want finally, not bare except:.\n\nBut if you’re doing an ultra-defensive wrapper around third-party callbacks where even KeyboardInterrupt should be captured (rare, but possible), then bare except: can be justified. When I do it, I make it loud and I re-raise.\n\npython\nimport logging\n\nlogger = logging.getLogger(‘plugin-host‘)\n\ndef runplugin(plugincallable):\n try:\n plugincallable()\n except: # Intentionally broad: plugin boundary\n logger.exception(‘Plugin crashed‘)\n raise\n\n\nThe key is that I’m treating it as a trust boundary, not normal application logic.\n\n## Debugging and Observability in 2026: Don’t Print, Capture Context\nIn modern Python services, the main reason to catch exceptions is not to hide failures. It’s to:\n\n- Add context (what input, what user, what request id).\n- Clean up resources.\n- Convert low-level exceptions into domain-level errors.\n- Report failures with enough detail to fix them.\n\nHere’s a practical comparison of older habits vs what I expect in a current codebase.\n\n

Goal

Traditional approach

Modern approach (what I recommend)

\n

\n

Basic error visibility

print(e)

Structured logging with stack traces (logger.exception)

\n

Debugging context

None or vague string

Include identifiers (request id, user id, file path)

\n

Error propagation

Swallow and continue

Re-raise or return a clear error result

\n

Monitoring

Manual log grepping

Error aggregation + traces (common in service stacks)

\n

Developer feedback

Find out later

Fast feedback via tests, linters, and type checkers

\n\nIn practice, except Exception as e: is the entry point to better observability because it gives you e for:\n\n- Exception chaining (raise DomainError(...) from e)\n- Attaching extra data in logs\n- Recording metrics (counts by exception type)\n\nAI-assisted workflows also change how I write handlers. If you use an assistant to help triage incidents, it performs much better when the logs include:\n\n- The exception type\n- The full stack trace\n- Key runtime context (inputs, environment, feature flags)\n\nA bare except: that prints “An error occurred” is basically the worst-case input for any debugging workflow—human or AI.\n\n### A concrete “modern” handler I actually like\nHere’s a realistic pattern for a worker that processes messages. The goal is: don’t lose stack traces, don’t swallow shutdown, and don’t spam logs for expected failures.\n\npython\nimport logging\n\nlogger = logging.getLogger(‘worker.orders‘)\n\nclass RetryableError(RuntimeError):\n pass\n\nclass PermanentError(RuntimeError):\n pass\n\ndef handlemessage(msg: dict) -> None:\n # Your domain logic here.\n raise RetryableError(‘Temporary upstream outage‘)\n\ndef workerloop(messages):\n for msg in messages:\n try:\n handlemessage(msg)\n except RetryableError as e:\n logger.warning(‘Retryable failure msgid=%s err=%r‘, msg.get(‘id‘), e)\n # Requeue or backoff would happen here.\n except PermanentError as e:\n logger.error(‘Permanent failure msgid=%s err=%r‘, msg.get(‘id‘), e)\n # Send to dead-letter queue or mark failed.\n except Exception:\n # Unexpected. Keep full traceback.\n logger.exception(‘Unexpected crash msgid=%s‘, msg.get(‘id‘))\n raise\n\n\nNotice what’s missing: no bare except:, and no “swallow everything and keep going” without a plan. If the code hits an unknown exception, I treat it as a bug worth stopping for (or at least worth escalating loudly).\n\n## Subtle Edge Cases: else, finally, Chaining, and ExceptionGroup\nIf you’re already writing serious Python, the difference between these forms matters most in the edges.\n\n### else keeps the happy path clean\nI use else when the success path needs to be clearly separated from the exception path.\n\npython\nimport json\n\npayload = ‘{"customerid": "cust20391"}‘\n\ntry:\n data = json.loads(payload)\nexcept json.JSONDecodeError as e:\n print(f‘Bad JSON payload: {e}‘)\nelse:\n # Runs only if no exception was raised.\n print(‘Customer id:‘, data[‘customerid‘])\n\n\nThis is not just style. It reduces the chance that you accidentally reference partially-initialized variables from inside the try block (a very common source of secondary failures).\n\n### finally is for cleanup, not error masking\nWhen you need cleanup, use finally. Avoid catching exceptions just to run cleanup.\n\npython\nfrom pathlib import Path\n\nfilehandle = None\ntry:\n filehandle = Path(‘report.txt‘).open(‘w‘, encoding=‘utf-8‘)\n filehandle.write(‘report line\n‘)\nfinally:\n if filehandle is not None:\n filehandle.close()\n\n\nIn modern code, context managers (with) are usually cleaner, but finally still matters in some low-level cases.\n\nAlso: if you’re tempted to do this, pause:\n\npython\ntry:\n dostuff()\nexcept Exception:\n cleanup()\n\n\nYou almost always meant finally, not except. Cleanup should run on success and failure.\n\n### Exception chaining: keep root cause and add meaning\nIf you catch a low-level exception and want to raise a domain-specific one, chain it.\n\npython\nclass PaymentConfigurationError(RuntimeError):\n pass\n\ndef loadpaymentkey(config: dict) -> str:\n try:\n return config[‘payment‘][‘apikey‘]\n except KeyError as e:\n raise PaymentConfigurationError(‘Missing payment.apikey in config‘) from e\n\n\nThis preserves the original cause while improving the message for the caller.\n\nIn real code, I often chain when I cross a boundary:\n\n- From a third-party library exception to my own domain exception.\n- From a low-level I/O error to a “failed to load settings” error.\n- From a parsing error to a “invalid user input” error.\n\nChaining is a gift to Future Me. When I’m on-call, it’s the difference between “I know exactly which underlying call failed” and “I have a vague domain error with no root cause.”\n\n### ExceptionGroup and except (Python 3.11+)\nConcurrent code can raise multiple exceptions as a group. If you’re working with modern async/task groups, you may see ExceptionGroup.\n\nA minimal example:\n\npython\ndef runtasks():\n raise ExceptionGroup(\n ‘multiple failures‘,\n [ValueError(‘Bad input‘), RuntimeError(‘Service unavailable‘)]\n )\n\ntry:\n runtasks()\nexcept ValueError as group:\n print(‘Handled value errors:‘, [str(e) for e in group.exceptions])\nexcept RuntimeError as group:\n print(‘Handled runtime errors:‘, [str(e) for e in group.exceptions])\n\n\nThis is another reason I avoid overly broad handlers: modern Python gives you tools to be precise, even when failures come in batches.\n\nOne practical note: if you’re catching broadly around concurrent code, you should decide whether you want to handle the entire group or specific parts of it. “Catch everything” gets even more dangerous when “everything” can be multiple unrelated failures at once.\n\n## Async and Cancellation: The Trap Even except Exception Can Fall Into\nUp to now I’ve been pretty bullish on except Exception as e: as the safe default broad catch. It usually is. But there’s one modern corner where you still need to think: async cancellation.\n\nIn async code, cancellation is often implemented by raising a cancellation exception (commonly asyncio.CancelledError). In many Python versions, that cancellation exception inherits from Exception. That means this pattern can accidentally block cancellation:\n\npython\nasync def dowork():\n ...\n\ntry:\n await dowork()\nexcept Exception:\n # This might swallow cancellation in async code.\n logandcontinue()\n\n\nWhat I do in async-heavy codebases is treat cancellation like shutdown signals: never swallow it. The simplest pattern is to special-case cancellation and re-raise immediately:\n\npython\nimport asyncio\nimport logging\n\nlogger = logging.getLogger(‘async-worker‘)\n\nasync def runonce():\n ...\n\nasync def mainloop():\n while True:\n try:\n await runonce()\n except asyncio.CancelledError:\n # Always let cancellation propagate.\n raise\n except Exception:\n logger.exception(‘Unexpected failure in mainloop‘)\n raise\n\n\nIf you don’t work with async code, you can ignore this section. If you do, it’s one of the most common causes of “why won’t this service stop?” issues even when developers avoided bare except:.\n\n## Alternative Approaches: Sometimes You Don’t Need except at All\nA lot of broad exception handling shows up because developers are trying to express one of these intentions:\n\n- “If it fails, treat it as missing.”\n- “Try best-effort cleanup.”\n- “Run this optional feature, but don’t break the main flow.”\n\nIn those cases, you can often write clearer code by using tools that are explicit about intent.\n\n### contextlib.suppress for truly ignorable errors\nIf you want to ignore a specific exception (not everything), contextlib.suppress is a clean option.\n\npython\nfrom contextlib import suppress\nfrom pathlib import Path\n\nwith suppress(FileNotFoundError):\n Path(‘optional-cache.json‘).unlink()\n\n\nThat reads like English: “Delete this file; if it’s not there, that’s fine.”\n\n### dict.get (and friends) for “missing is normal”\nIf you’re catching KeyError just to return a default, the dictionary already has a built-in for that.\n\npython\ntimeout = config.get(‘timeoutseconds‘, 30)\n\n\nSame for attribute access: getattr(obj, ‘name‘, None) is often cleaner than catching AttributeError.\n\n### “Optional features” should still log something\nIf you have optional integrations (metrics, tracing, best-effort reporting), it can be okay to keep the main program running even if the optional feature fails. But I still want visibility.\n\npython\ntry:\n sendmetric(‘job.completed‘, 1)\nexcept Exception:\n # Do not fail the job because metrics are down, but do record it.\n logger.exception(‘Metrics emission failed‘)\n\n\nNotice that this is a deliberate choice: metrics failure is not business failure. In other parts of the codebase, the opposite may be true. The point is: handle intentionally.\n\n## Performance Considerations: Exceptions Are Expensive (But Don’t Panic)\nPerformance is rarely the main reason to choose except: vs except Exception as e:. The bigger issue is correctness and observability. But it’s worth knowing a few practical things:\n\n- Raising exceptions is relatively expensive compared to a normal branch. As a rough intuition: it can be multiple times slower than an if check, and it tends to allocate objects and capture traceback state.\n- Catching exceptions in a tight loop as part of normal behavior (for example, using exceptions to detect the end of iteration or parsing failures in a hot path) can become a real cost at scale.\n- The fix is usually not “never use exceptions.” It’s “don’t use exceptions for expected control flow in hot paths.”\n\nA classic example: parsing. If invalid input is common, validate up-front where practical rather than relying on exceptions for most cases. If invalid input is rare, raising an exception is fine and can actually keep the happy path cleaner.\n\nThe except: vs except Exception as e: choice doesn’t change the cost much; it changes the blast radius and the chance of swallowing shutdown/cancellation.\n\n## Common Pitfalls (And the Fixes I Actually Use)\nThese are the patterns I see repeatedly in real code reviews, plus how I correct them.\n\n### Pitfall 1: Catching too broadly too early\nThis is the “wrap everything in try/except and keep going” anti-pattern.\n\npython\ntry:\n a = stepone()\n b = steptwo(a)\n c = stepthree(b)\nexcept Exception:\n return None\n\n\nFix: isolate the risky operation so you don’t accidentally hide where it failed, and so you can handle each step differently if needed.\n\npython\na = stepone()\n\ntry:\n b = steptwo(a)\nexcept KnownProblem as e:\n return fallback(e)\n\nc = stepthree(b)\n\n\n### Pitfall 2: Swallowing exceptions without recording them\nIf you truly want to ignore something, it should be specific and documented (and ideally very rare). If it’s not specific, you usually want at least a warning and a counter.\n\nFix: either narrow the exception type or log it with stack trace.\n\n### Pitfall 3: Logging without traceback\nlogger.error(str(e)) loses the traceback, which is usually the most important part.\n\nFix: use logger.exception(...) inside an except block, or pass excinfo=True.\n\npython\ntry:\n dothing()\nexcept Exception:\n logger.exception(‘dothing failed‘)\n raise\n\n\n### Pitfall 4: Catching Exception in async code and breaking cancellation\nAs covered earlier, cancellation may be an Exception.\n\nFix: re-raise cancellation explicitly before your general handler.\n\n### Pitfall 5: Turning real failures into mysterious partial success\nThis happens when code catches broadly and returns a default that looks legitimate.\n\npython\ntry:\n return fetchuser(userid)\nexcept Exception:\n return {}\n\n\nFix: return an explicit error result (or raise a domain error) so callers don’t mistake failure for an empty user. If you can’t change the return type, at least log loudly and consider failing fast.\n\n## A Rule Set You Can Apply Without Overthinking\nWhen I’m deciding between except:, except Exception as e:, and something more specific, I use a small checklist.\n\n1. If you’re inside normal application logic, don’t use bare except:.\n2. If you don’t have a specific exception type in mind, use except Exception as e: and either re-raise or return a clear error.\n3. If Ctrl+C should stop the program (CLIs, scripts, dev tools), make sure you are not catching KeyboardInterrupt by accident.\n4. If your code needs to exit with a status code, do not swallow SystemExit.\n5. If you must catch broadly at a boundary (plugin host, last-resort crash handler), log the full traceback and then re-raise or exit.\n\nIf you adopt only one practice, adopt this: use except Exception as e: instead of except: as your default broad catch, and treat anything broader as a deliberate design decision.\n\nWhen you’re cleaning up an older codebase, I usually start by searching for bare except: and asking one question at each site: “Am I willing to catch Ctrl+C and program exits here?” If the honest answer is “no” (and it almost always is), switching to except Exception as e: is a fast, low-risk improvement that immediately restores sane shutdown behavior and better debugging signals.\n\nThat single line won’t prevent every failure, but it will prevent a particularly expensive class of failures: the ones that keep running silently while you’re trying to figure out why nothing works.\n\n## Expansion Strategy\nAdd new sections or deepen existing ones with:\n- Deeper code examples: More complete, real-world implementations\n- Edge cases: What breaks and how to handle it\n- Practical scenarios: When to use vs when NOT to use\n- Performance considerations: Before/after comparisons (use ranges, not exact numbers)\n- Common pitfalls: Mistakes developers make and how to avoid them\n- Alternative approaches: Different ways to solve the same problem\n\n## If Relevant to Topic\n- Modern tooling and AI-assisted workflows (for infrastructure/framework topics)\n- Comparison tables for Traditional vs Modern approaches\n- Production considerations: deployment, monitoring, scaling\n\n### How I apply this in real projects (the part that makes it stick)\nWhen people ask “Okay, but what do you actually do?” my answer is: I treat exception handling like API design. The handler is a contract. If it’s too broad, it becomes unpredictable. If it’s too narrow, it becomes brittle. The sweet spot is: specific where it matters, broad only at boundaries, and never broad in a way that breaks shutdown.\n\nIn practice, that means I end up with three layers of defense:\n\n1. Local handling for expected problems (validation errors, missing files, parsing failures).\n2. Boundary handling for unexpected problems (log context, keep tracebacks, propagate).\n3. Process-level handling for last-resort crash reporting (log, flush, exit).\n\nThe except: vs except Exception as e: choice is mostly about making sure layer (2) doesn’t accidentally turn into layer (3) or break shutdown semantics.\n\n### Library code vs application code (different responsibilities)\nOne nuance that improves a lot of codebases quickly: libraries and applications have different exception-handling jobs.\n\n- In application code (CLIs, workers, services), it’s normal to catch exceptions at boundaries to add context and log.\n- In library code, broad catching is much riskier because you don’t know what the caller wants. Libraries should usually raise meaningful exceptions and let the application decide how to handle them.\n\nA practical rule I follow: inside libraries, I catch only when I can add meaningful context or normalize a low-level exception into a stable public exception type. Otherwise, I let it bubble up.\n\npython\nclass StorageError(RuntimeError):\n pass\n\ndef saveblob(storageclient, key: str, data: bytes) -> None:\n try:\n storageclient.put(key, data)\n except TimeoutError as e:\n raise StorageError(f‘Timed out saving blob key={key!r}‘) from e\n\n\nNotice: I didn’t catch everything, I caught a specific low-level failure I want to present as part of my library’s public API.\n\n### Retrying responsibly (broad catches can create retry storms)\nRetries are where bad exception handling becomes expensive. If you catch everything and retry everything, you can turn a single bug into a thundering herd of repeated failures.\n\nWhen I implement retries, I choose deliberately:\n\n- Retryable: transient network failures, timeouts, rate limits, temporary upstream outages.\n- Not retryable: validation errors, auth failures, missing config, programming errors.\n\nThat maps naturally to exception types. It’s another reason to avoid bare except:: it makes it too easy to treat all failures as retryable and create runaway load.\n\nA simple pattern looks like this (even without any retry library):\n\npython\nimport time\n\ndef fetchwithretry(fetch, attempts: int = 3, delayseconds: float = 0.5):\n lasterr = None\n for in range(attempts):\n try:\n return fetch()\n except TimeoutError as e:\n lasterr = e\n time.sleep(delayseconds)\n raise RuntimeError(‘Fetch failed after retries‘) from last_err\n\n\nIf fetch() raises KeyboardInterrupt or SystemExit, they still terminate the program. If it raises ValueError because your code is wrong, you see it immediately instead of wasting time retrying.\n\n### Testing exception paths (so handlers don’t rot)\nException handlers are code paths, and code paths without tests tend to rot. The two failures I see most are:\n\n- The handler logs the wrong thing (or nothing).\n- The handler swallows exceptions that should propagate.\n\nEven lightweight tests help. For example, verify that your boundary handler re-raises and preserves behavior for shutdown signals. Conceptually, I want tests that assert:\n\n- “Normal runtime errors get logged.”\n- “Cancellation/shutdown is not swallowed.”\n\nWhen you have tests for that, except Exception as e: becomes a safe, enforceable rule instead of a style preference.\n\n### Tooling guardrails (the easiest win in teams)\nIf you work on a team, relying on everyone’s memory is fragile. I like adding guardrails so the default is safe.\n\nCommon lint rules in the Python ecosystem flag bare except: because it’s a known footgun. If your project uses a linter, enabling the rule is usually low friction and high impact.\n\nEven if you don’t enable a strict rule, a simple internal guideline like “no bare except: outside process boundaries” catches a surprising number of production issues early.\n\n### The one-sentence takeaway I give juniors\nIf I have to compress all of this into one sentence I’d put on a sticky note, it’s this:\n\nUse except Exception as e: when you mean “handle errors,” and use bare except: only when you truly* mean “handle absolutely everything,” including Ctrl+C and exits—and you’d better log and re-raise when you do.

Scroll to Top