Every API client is different. They are shaped by many things, including:

People have told me that they love github3.py and how I designed it. Personally, I feels there is room for significant improvement. Many of the deficiencies sought to fix other problems. Let's explore.

Overloaded use of None

None is the favorite sentinel in Python. It can have any number of semantic meanings depending on the context. In github3.py , it also had several of its own meanings:

a request the user made for an object returned a 404

GitHub's API returned null for a specific attribute

for a specific attribute the representation of the current object didn't contain the attribute

The first and last cases were what caused me to make some ... questionable design decisions.

None returned because of 404 GitHub's API returns 404 for a few cases: the route is incorrect, e.g., /userss/foo (note the extra s in userss ) the resource doesn't exist, e.g., /users/does-not-exist-123456789 the user doesn't have permission to see the resource, e.g., /repos/company/private-repo But the API doesn't tell us why. Because of this, we cannot guide the user towards remediation. It seemed best when I made the library to just return None . Representing a 404 (on some calls) as None started to irk me. As a user of my own library, I had written a lot of code that looked like user = gh . user ( 'sigmavirus24' ) if user is not None : # Do something repository = gh . repository ( 'sigmavirus24' , 'github3.py' ) if repository is not None : # Do something I discovered the Null Object pattern while I was still working predominantly in Ruby. I thought that this could make all of those pesky checks for None go away. I'd finally be able to just code fearlessly while still being able to warn the user if it was necessary and, of course, surely my users would love it too! It turns out that implementing an object that behaves this way is pretty easy. So naturally, I threw it right into github3.py . Unfortunately, people have found this more confusing than not. I don't want to confuse my users. I don't want to burden them with extra cognitive decisions and context. I decided it's probably best to revert to just returning None . If you're wrinkling your nose at that, then let's talk through some alternatives: raise an exception In my opinion this is worse than returning None because then each of those accesses looks like: try : user = gh . user ( 'sigmavirus24' ) except github3 . exceptions . NotFound : # Handle 404 else : # Do something with user object At least by returning None, users can be reasonably sure they'll get a value they can handle. return an Option or Result type If this were Haskell or Rust that would be fantastic. I suspect, however, that this would be as confusing (if not more so) than the Null object pattern. To provide an example, this would start to look like this: user = gh . user ( 'sigmavirus24' ) . ok () . unwrap () That provides some assurance that the user will exist. There is no need for the guarding if conditions. But exception handling will have to happen somewhere since it isn't always safe to just unwrap a result.