# My Auth Layer Got Caught in a Messy Divorce: passlib vs. bcrypt

_Filed under: dependencies I trusted, and the betrayal that followed._

June 19, 2026 · 3 min · Amit Jethva

There's a special kind of 2am despair reserved for the moment a library you've used for years suddenly decides it doesn't recognize its own spouse. For us, that library was `passlib`, the spouse was `bcrypt`, and the divorce paperwork was a stack trace.

## The honeymoon

For ages, the standard incantation for "hash this password, please" was:

```python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
```

Beautiful. Declarative. The kind of code you copy from a tutorial and never think about again. `passlib` handled the `bcrypt` relationship for you. You didn't have to know the gritty details. It was a happy, abstracted marriage.

## The breakup

Then `bcrypt` hit version 4.0 and quietly moved out. It restructured its internals — removed an attribute (`__about__.__version__`) that `passlib` 1.7.4 had been leaning on to introduce its partner at parties.

So `passlib` shows up to do a routine password hash, turns to introduce `bcrypt`, reaches for the version string... and it's gone. Cue:

```
(trapped) error reading bcrypt version
AttributeError: module 'bcrypt' has no attribute '__about__'
```

It doesn't even crash cleanly. It _limps_. Sometimes it works. Sometimes it warns. It's the dependency equivalent of two people who are technically still living together but communicating exclusively through the dog.

## What actually fixed it

We stopped trying to save the marriage and let `bcrypt` speak for itself:

```python
import bcrypt

def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

def verify_password(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(password.encode(), hashed.encode())
```

No counselor. No abstraction layer translating between two parties who no longer speak. Just `bcrypt`, directly, doing the one thing we needed.

The bonus plot twist: this bit us _again_ later in `team_service.py`'s `accept_invite()`, where someone (me) had reflexively reached for `passlib` out of muscle memory. Old habits file for re-marriage.

## The moral

- **An abstraction over a dependency is itself a dependency** — now you have two relationships to maintain instead of one.
- When a wrapper library and the thing it wraps disagree about versions, the wrapper usually loses, and _you_ find out at runtime.
- We made it a house rule: **never `passlib.CryptContext` in new code. Use `bcrypt` directly.** It's in our `CLAUDE.md` now, in bold, like a restraining order.

Sometimes the most senior move is to delete the clever thing and just call the function.

---

_Amit Jethva is the CTO and co-founder of Nuvika Technologies Pvt Ltd, makers of [Fintropy](https://www.nuvikatech.com/Fintropy_Overview.html), a multi-cloud FinOps platform. Learn more at [nuvikatech.com](https://www.nuvikatech.com)._
