92.2 Strawberry: Code-First GraphQL with Type Hints
Right, so you’ve decided to build a GraphQL API in Python. Good choice. You’ve probably looked at the landscape and seen graphene, which is fine, but it feels a bit… declarative in a clunky way. You define your schema in a language that’s not quite Python, using classes that are a bit verbose. It works, but it doesn’t feel great. Enter Strawberry. It’s the “code-first” GraphQL library that actually embraces modern Python, specifically type hints, to make your schema definition not just tolerable, but genuinely pleasant. The core idea is brilliantly simple: your GraphQL schema is a direct reflection of your Python type-annotated classes and functions. No redundant schema language. It’s just Python.
Let’s get our hands dirty. First, obviously, install it: pip install 'strawberry-graphql[debug-server]'. The debug server is non-negotiable; you want that GraphiQL interface.
Your First Type and Schema
Think of a Strawberry type as a dataclass that’s been to finishing school. You use the @strawberry.type decorator, and then define your fields with Python type hints. Strawberry takes those hints and magically translates them to GraphQL types.
import strawberry
@strawberry.type
class Book:
title: str
author: str
@strawberry.type
class Query:
@strawberry.field
def get_book(self) -> Book:
return Book(title="The Hitchhiker's Guide to the Galaxy", author="Douglas Adams")
schema = strawberry.Schema(query=Query)
See? No String or ObjectType nonsense. It’s just str and Book. This is the “code-first” part. You’re writing Python code, and Strawberry generates the GraphQL Schema Definition Language (SDL) from it. You can even check it by running print(schema.as_sdl()). It’s not a separate entity you have to maintain; it’s a byproduct of your code. This is the way.
The Power of Resolvers
In the above example, get_book is a resolver function. Notice how the return type hint -> Book tells Strawberry everything it needs to know about what this field returns. The resolver’s job is to fetch the data. How you do that is your business—talk to a database, call another API, read a flat file, whatever. Strawberry just cares that you return an object that matches the type hint.
Now, let’s make it actually useful by adding an argument. Say we want to get a book by its ID.
@strawberry.type
class Query:
@strawberry.field
def get_book(self, id: strawberry.ID) -> Book:
# In real life, you'd fetch from a database here.
if id == "1":
return Book(title="Dune", author="Frank Herbert")
return None # GraphQL will handle this as null!
Strawberry is smart enough to take that Python-type id: strawberry.ID and make it a required ID! argument in GraphQL. Use strawberry.ID for clarity, but a simple str would work too. Want it to be optional? Standard Python stuff: id: strawberry.ID | None = None.
Mutations Are Just Verbs
Mutations are where you change data. In Strawberry, they’re just a different class. The pattern is identical.
@strawberry.input
class BookInput:
title: str
author: str
@strawberry.type
class Mutation:
@strawberry.mutation
def add_book(self, data: BookInput) -> Book:
# Here you'd save to your database and then return the saved object.
new_book = Book(title=data.title, author=data.author)
print(f"Pretending to save {new_book} to the DB...")
return new_book
# Don't forget to update your schema to include the mutation root!
schema = strawberry.Schema(query=Query, mutation=Mutation)
Notice we used @strawberry.input for the argument. This creates a GraphQL input type, which is the right way to do this. Using a regular @strawberry.type for inputs is a common rookie mistake—it’ll work until it suddenly doesn’t when your schema gets more complex. Input types exist for a reason; use them.
The Subscription Sweet Spot
This is where Strawbery, GraphQL, and async Python truly sing together. Subscriptions use WebSockets to push real-time data to the client. Strawberry makes this surprisingly straightforward, leveraging async generators.
import asyncio
@strawberry.type
class Subscription:
@strawberry.subscription
async def count(self, target: int = 10) -> int:
for i in range(target):
yield i
await asyncio.sleep(1)
This is a silly example, but it shows the mechanics. You define an async generator that yields values. Each yield is a payload sent to the client. The await asyncio.sleep(1) is the key—this is where you’d typically wait for a trigger from a message queue, a database change listener, or any other event source in a real application.
The Rough Edges and Pitfalls
It’s not all strawberries and cream (I’m sorry, I had to). Here’s what to watch for:
- Circular Imports: This is the big one. Your types will reference each other (
Bookhas anAuthor,Authorhas a list ofBooks). If you define everything in one file, fine. The second you split into multiple files, you’ll run into circular import hell. The solution is to use Strawberry’s lazy typing withstrawberry.LazyType["OtherType"]or by using string annotations (-> "Author"). - N+1 Queries: This is a GraphQL universal problem, not just Strawberry’s. A query for 10 books and their authors could result in 1 query for the books and 10 individual queries for each author. You absolutely need a strategy for this. The best practice is to use a DataLoader to batch those subsequent requests. Strawberry has excellent, async-first support for them.
- Async vs Sync: Strawberry supports both, but you should pick one and be consistent. Mixing them can lead to performance bottlenecks. If you’re doing anything I/O-bound (which, in a GraphQL resolver, you almost always are), go all-in on async.
The bottom line? Strawberry is the library that treats you like a competent Python developer. It doesn’t force you to learn a new DSL; it uses the tools Python already gives you. It’s powerful, it’s elegant, and it’s probably what the graphene designers wished they’d built first.