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:

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:

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, a multi-cloud FinOps platform. Learn more at nuvikatech.com.