Changing the unchangeable

Changing the unchangeable

How do you extend a library's built-in function?

Let me give you an example, from Django. You know, the web app framework.

I've built about fourteen billion webapps using Django. Something like that, I lost count awhile back. And like many webapp libraries, Django gives you tools for creating and managing user accounts.

So you can build a website called, say, powerfulpython.com. And give people accounts on this website, so they can log in and access their courses. For example.

Anyway:

Did you know that by default, Django doesn't ignore case in usernames? So "bob", "Bob", "BOB" will be three different accounts.

And even more fun:

It doesn't ignore whitespace. So "bob", " bob" and "bob " are three different accounts, too.

Guess how many times I had to manually fix someone's login because they'd hit the stupid spacebar.

Actually, it wasn't too many times. Because by the third time, I was already tired of dealing with it. So I decided to fix it at the source.

In Django, there's a class method called User.objects.create_user(). You can think of it as just a function, really. And in my brazen opinion, it's the only acceptable way of creating a new user account in Django, for reasons.

So back to my problem. I want all user account names to be normalized to lowercase, and stripping out whitespace, and have THAT name be stored in the database. And I want to make sure neither me nor any of my staff can forget that in the future.

Here's how I solved it. First, I made a function to normalize the raw username value:

  1. def norm_username(username):
  2. return username.strip().lower()

Simple. Then, I wrote my own create_user function:

  1. def create_user(username, email, password):
  2. username = norm_username(username)
  3. return _original_create_user(username, email, password)

What, you ask, is _original_create_user()? Why, it's this:

  1. _original_create_user = User.objects.create_user

Okay... so this works great. But why, pray tell, did I even create this variable? Why did my create_user() not just call User.objects.create_user() directly?

Think about it a moment... because I did this for a reason. Can you think of why?

I'll give you one more second...

Time's up. Here's why:

Because I don't want anyone in the code base to call the original version at all. Because if some developer doesn't realize (or forgets) that this webapp has a custom create_user() function, and they use that built-in User.objects.create_user()...

It's possible we'll end up with "bob", " bob" and "Bob".

Nothing against you if your name is Bob. But that ain't happenin'. So to enforce this, I also had these lines:

  1. # mask the main create_user
  2. def disabled_create_user(*args, **kwargs):
  3. assert False, 'Use base.util.create_user instead'
  4. _original_create_user = User.objects.create_user
  5. User.objects.create_user = disabled_create_user

See what I did? Now, if anyone in the code base forgets or just doesn't know we have our own create_user(), and uses what Django's official docs tell you to use...

Boom. Stack trace, telling you exactly what to do instead.

This is called "patching", or sometimes "monkey patching". And while it's possible to abuse it, it's also the basis for some REALLY useful code constructs in Python. Just like I'm showing you here.

And it all comes out of understanding Python at a deeper level. When you learn a handful of principles that are "beyond the basics" of what most Python programmers will ever learn...

It opens up new worlds of what you can do in your code.



Book Bootcamp