My first Python library has nearly 500 GitHub stars. To get a track's name out of it, you wrote this:
print(item["item"]["data"]["name"])
That one line explains most of what I'd do differently.
I started SpotAPI when I was 16. I'd spent a long time working with Spotify's unofficial web-player API, and I had a pile of code that worked, so I bundled it into a library and put it on GitHub. Over the next two years it picked up nearly 500 stars while the code barely changed. I didn't maintain it as well as I should have, and other people sent most of the fixes that kept it alive.
Version 2 is a typed, async rewrite. Here's what I changed. None of it is specific to Spotify.
1. Return models, not nested dicts
Version 1 handed back whatever the server sent:
# v1
for batch in song.paginate_songs("weezer"):
for item in batch:
print(item["item"]["data"]["name"])
With an undocumented API, that couples every user's script to a response shape nobody promised to keep. Now results are typed models:
# v2
results = await Public.search("weezer", limit=5)
for track in results.tracks:
print(track.name, track.uri)
You get autocomplete and type checking, and when Spotify changes a response, the fix lives in the library instead of in every script that depends on it.
2. Reading an attribute should never make a network call
In v2, reading a model never triggers a hidden HTTP request. If Spotify didn't send a field, it stays optional and you decide whether to go and fetch it.
Lazy-loading attributes feel convenient until you hit the N+1 problem in a loop. In async code it's worse, because a property can't await. Network access should look like network access.
3. Offer a one-shot call and a context manager
# One call: cleans up after itself
results = await Public.search("Daft Punk", limit=5)
# Many calls: share one session
async with Catalogue() as spotify:
results = await spotify.search("Daft Punk")
artist = await spotify.get_artist(results.artists[0].uri)
print(artist.name)
The one-shot covers the common case without leaking connections. The context manager makes ownership explicit when you're making several calls. Resource lifetime is where async libraries tend to go wrong, so I wanted both paths to be hard to misuse.
4. A row is not the thing it points to
A playlist can contain the same track twice. If your API says remove(track_uri), which copy goes?
SpotAPI models each occurrence of a track in a playlist, with its own ID. Playlist.items() returns occurrences, and remove_occurrences() and move_occurrences() act on them, so you can edit one copy without touching its twin.
The same bug shows up in shopping carts, queues and to-do lists. Model what the user is actually pointing at, not the entity it refers to.
5. If you wrap something unofficial, plan for it to break
SpotAPI uses the same web-player endpoints as open.spotify.com, and Spotify can change them without telling anyone. Here's how I design around that:
- Discover at runtime, cache by build. The first request finds Spotify's current web-player queries, and later runs reuse them for the same Spotify build. The tradeoff is a slower first run.
-
One exception to catch. Library failures are
SpotAPIError, so your code can tell "the library broke" from "my code broke". - Opt-in live tests. The default test run (441 passing) doesn't depend on a third party's uptime.
- Written-down boundaries. The docs list what I verified and what I didn't.
What I got wrong the first time
Stars aren't maintenance. I was slow to answer issues and cut releases, and people sometimes had to ask me to update the PyPI package.
Releases now go out from GitHub Actions with PyPI attestations, so you can verify where a package came from.
Try it in 30 seconds
You don't need to care about Spotify to see what a typed async client feels like. You need Python 3.11+:
pip install spotapi
spotapi search "Ca$h YP"
There's no API key, login or config. The first run is slower while it discovers queries. For something nicer to look at, there's a terminal UI with artwork:
pip install "spotapi[demo]"
spotapi discover "Ca$h YP"
What I'd like feedback on
-
Layering: Is
PublicvsCatalogue(one-shot vs shared session) clear, or did I over-split it? - Hidden I/O: Do you agree that models shouldn't do hidden I/O, or have you found a case where lazy loading was worth it?
- Unofficial APIs: How do you keep a client for an unofficial API maintainable?
SpotAPI uses unofficial endpoints, may conflict with Spotify's terms, and doesn't stream or download audio. See the legal notice before using it. It's GPL-3.0.

Top comments (0)