S
Saurav Danej
90-Day AI/ML LinkedIn Content System
← All days
14
Day 14 of 90Python

Errors, exceptions & week 2 wrap

POST 1 of 5 MorningPythonConcept

Fail loud, fail fast — never silently

I've debugged a lot of Python code over the years, and one anti-pattern accounts for more wasted hours than any other.

try:
    do_thing()
except Exception:
    pass

This is the silent failure. Catches every error — including the typo, the import error, the file-not-found, the network timeout, the ML divergence — and swallows it without a sound. Your program limps onward, missing parts, and you spend hours wondering why nothing trains.

The rule, which I'll state in big bold mental letters — fail loud, fail fast, never silently.

If an error happens, you want to know. Loudly. With a stack trace. As close to the cause as possible. Anything else trades short-term smoothness for long-term debugging hell.

The right shape is specific exception handling.

try:
    json.loads(text)
except json.JSONDecodeError as e:
    log.warning('bad json', extra={'err': e})
    return None

Notice three things. The except is for ONE specific exception type. The handler does something useful — logs the cause for later analysis. The handler returns a defined fallback. Anything else (TypeError, KeyError, MemoryError) is NOT caught here. It propagates up the stack to a layer that DOES know how to handle it, or crashes the program with a useful trace.

Never 'except Exception:'. Almost never. The handful of cases where it's right (top-of-loop request handlers in a web server that must keep serving even on user-code bugs) are cases where you log the full traceback before continuing.

Never bare 'except:'. Catches even KeyboardInterrupt and SystemExit. Your program becomes uninterruptible.

The guideline is one sentence — catch the most specific exception you can handle, and re-raise everything else. Your future self, debugging at 3am, will thank you for the loudness.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonExceptions
POST 2 of 5 MiddayPythonDeep dive

Custom exceptions — when and how

Most code I see either uses too many built-in exceptions (raise ValueError everywhere) or invents a custom one for every condition. The middle ground is right.

Define a custom exception when:

The error is specific to your domain. A retrieval failure in a RAG system isn't a ValueError — it's a RetrievalError. The name carries information.

Callers should be able to catch it precisely. If users of your code want to recover from one type of failure but not others, they need distinct exception types to discriminate.

You want a base class for a family of related errors. RAGError as base, RetrievalError, ChunkingError, EmbeddingError as subclasses. Now callers can 'except RAGError' to catch any of them, or 'except RetrievalError' for one specific kind.

The pattern looks like:

class RAGError(Exception):
    pass

class RetrievalError(RAGError):
    pass

class ChunkingError(RAGError):
    pass

Four lines. Three exception types. A clear hierarchy.

Don't subclass Exception for every tiny case. Three to five domain exceptions usually cover an entire project. More than that and you're micro-typing errors at a cost that callers won't pay attention to.

A related principle — don't reuse built-in exceptions for unrelated meanings. Don't raise ValueError for a retrieval failure just because you don't want to define a class. Define the class. The 30 seconds of effort gives every caller a precise way to handle the failure.

Document what your custom exceptions mean. A docstring on the class. When it's raised, what callers should typically do. Three lines per exception class, max.

Properly named, hierarchical exceptions are an underrated form of API design. They tell callers what can go wrong without forcing them to read the implementation.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonAdvanced
POST 3 of 5 AfternoonPythonCode

raise from — keep the original error visible

When you wrap a third-party error in a domain error, do this — raise YourError(...) FROM e.

The 'from' keyword links the new exception to the original cause. Python's traceback shows both. The reader sees the underlying network error AND the high-level domain error in one stack trace.

Look at the snippet. fetch() catches a requests.RequestException and wraps it in a domain RetrievalError. Without 'from e', the traceback shows the domain error and a hint that 'during handling of the above exception, another exception occurred'. With 'from e', it shows 'the above exception was the direct cause of the following exception' — explicit chaining.

The difference seems minor in writing. In a 3am debugging session, it's the difference between 'I can see the network failure that caused the wrap' and 'I have to re-read the code to find what called this'.

Use 'raise from':

When wrapping a low-level exception in a high-level one. Almost always.

When translating an exception from one library's exception class to your own. Same reason.

Don't use 'raise from':

When the original exception isn't relevant. Use 'raise from None' to suppress the link. Useful when the internal exception is an implementation detail and you don't want to leak it in tracebacks.

A common pattern in libraries — internal code raises a generic exception that the library's public API catches and wraps with 'raise from None' to hide the implementation. Callers see only the documented exception type.

For application code, you almost always want to keep the chain. Future-you debugging an issue will know where to look.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonExceptions
POST 4 of 5 EveningPythonTip

Use ExceptionGroup for parallel failures

Python 3.11 added ExceptionGroup, and most developers haven't met it yet. It changes how you handle parallel work.

Before 3.11, if you fanned out 50 tasks and 3 failed, the conventional approach was 'first failure wins'. asyncio.gather() raised the first exception; the other two were lost.

ExceptionGroup fixes this. Now when you use TaskGroup (the structured-concurrency replacement for gather), partial failures get bundled into a single ExceptionGroup that contains ALL the failed tasks' exceptions.

from asyncio import TaskGroup

async with TaskGroup() as tg:
    for url in urls:
        tg.create_task(fetch(url))

If 3 of 50 fetches fail, you don't lose 47 successes and you don't lose 2 of the 3 failures. The ExceptionGroup carries all 3 errors. You can inspect them, log them, retry only the failed ones.

A new keyword goes with this — except*. (Yes, with the asterisk.) It catches inside an ExceptionGroup, matching by type:

try:
    ...
except* requests.HTTPError as eg:
    handle_http_failures(eg.exceptions)
except* ValueError as eg:
    handle_validation_failures(eg.exceptions)

Each 'except*' catches matching exceptions from inside the group. The unmatched ones re-raise.

Where this matters in AI/ML work — batched LLM calls. You fan out 100 prompts to an API; some hit rate limits, some hit content filters, some fail for transient network reasons. Without ExceptionGroup, you only see the first failure. With it, you can categorise and retry intelligently.

The shape of concurrent error handling has changed. If you're on 3.11+, learn TaskGroup + ExceptionGroup. If you're stuck on older Python, this is one more reason to upgrade.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#Python311
POST 5 of 5 NightCareerRecap

Week 2 done — Python that scales

End of week two. 14 days in, 70 posts shipped, zero misses. The dataset of work is starting to feel substantial.

What week 2 added on top of week 1.

OOP that's actually idiomatic — classes when they earn their weight, dunder methods to integrate with Python's protocols, composition over inheritance, __repr__ on every class.

Decorators demystified — @ is sugar for 'foo = decorator(foo)', three shapes (plain, parameterised, stacked), @functools.wraps non-negotiable.

Generators as the laziness primitive — yield/next, generator expressions, streaming pipelines, yield from.

Context managers — with-statements, two ways to author them, ExitStack for runtime composition.

Types — eight primitives that cover 95% of typed Python, dataclasses to kill boilerplate, mypy strict in CI.

Exceptions — fail loud, custom hierarchy when justified, raise from to keep traces meaningful, ExceptionGroup for parallel failures.

On the meta level, this week was the 'production Python' tier. Most modern frameworks (FastAPI, PyTorch, LangChain, Django) are written using these constructs. After this week, you can read the source code of the libraries you import and not feel lost.

A quick reality check on the sprint pace. We're 15.5% of the way through the 90 days. Two weeks of Python. Done. Now we head into DSA — Big-O, arrays, strings, hash maps, stacks, recursion, search, sort, trees, graphs, DP. Two weeks. The foundation that ML interviews and elegant code share.

After that, Week 5 begins the actual ML stack — NumPy, Pandas, EDA, classical ML, deep learning, transformers, RAG, agents, automation, career. The march continues.

Thank you for showing up this week. See you tomorrow for Day 15.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#90DaysOfAI