docs/types/maybe.md
In GraphQL, there's an important distinction between a field that is null and
a field that is completely absent from the input. Strawberry's Maybe type
allows you to differentiate between these states:
Maybe is the recommended way to handle optional input fields in Strawberry. If
you're using strawberry.UNSET for this purpose, we encourage migrating to
Maybe for better type safety and clearer semantics. See
Migrating from UNSET for details.
For Maybe[str]:
Some("hello")NoneFor Maybe[str | None] (when you need to handle explicit nulls):
Some("hello")Some(None)NoneThis is particularly useful for update operations where you need to distinguish between "set this field to null" and "don't change this field at all".
The design is inspired by Rust's
Option<T> type and similar patterns
in functional programming languages like Haskell's Maybe and Scala's Option.
strawberry.Maybe solve?Consider this common scenario: you have a user profile with an optional phone number, and you want to provide an update mutation. With traditional nullable types, you can't distinguish between:
Both would be represented as phone: null in your GraphQL mutation.
Here's how to use Maybe in your Strawberry schema:
import strawberry
@strawberry.input
class UpdateUserInput:
name: str | None = None # Traditional optional field
phone: strawberry.Maybe[str | None] # Maybe field
@strawberry.type
class User:
name: str
phone: str | None
@strawberry.type
class Mutation:
@strawberry.mutation
def update_user(self, user_id: str, input: UpdateUserInput) -> User:
user = get_user(user_id) # Your user retrieval logic
# Traditional optional field - only update if provided
if input.name is not None:
user.name = input.name
# Maybe field - check if field was provided at all
if input.phone is not None: # Field was provided
user.phone = input.phone.value # Access the actual value
# If input.phone is None, the field wasn't provided - no change
return user
When a Maybe field has a value (including null), it's wrapped in a Some()
container:
# Field provided with a string value
phone = strawberry.Some("555-1234")
print(phone.value) # "555-1234"
# Field provided with null value
phone = strawberry.Some(None)
print(phone.value) # None
# Field not provided at all
phone = None
print(phone) # None
When you use Maybe in your schema, it appears as a nullable field in GraphQL:
@strawberry.input
class UpdateUserInput:
phone: strawberry.Maybe[str | None]
Generates this GraphQL schema:
input UpdateUserInput {
phone: String
}
Maybe is most commonly used in input types for update operations:
@strawberry.input
class UpdatePostInput:
# Maybe[T] - value or absent, null is INVALID
# Use when the field must have a value if provided
title: strawberry.Maybe[str]
content: strawberry.Maybe[str]
published: strawberry.Maybe[bool]
# Maybe[T | None] - value, null, or absent
# Use when null is a valid value (e.g., to clear/remove the field)
tags: strawberry.Maybe[list[str] | None]
@strawberry.type
class Mutation:
@strawberry.mutation
def update_post(self, post_id: str, input: UpdatePostInput) -> Post:
post = get_post(post_id)
# Only update fields that were explicitly provided
if input.title is not None:
post.title = input.title.value
if input.content is not None:
post.content = input.content.value
if input.published is not None:
post.published = input.published.value
if input.tags is not None:
post.tags = input.tags.value # Could be None to clear tags
return post
The key distinction is between Maybe[T] and Maybe[T | None]:
Maybe[T]: Two states - value provided or absent. Use when the field
cannot be set to null (e.g., a required title that you either update or
leave unchanged).Maybe[T | None]: Three states - value provided, null provided, or
absent. Use when null is meaningful (e.g., clearing an optional phone
number).Both Maybe[T] and Maybe[T | None] generate the same GraphQL schema (a
nullable field). The distinction between them is enforced by Strawberry's
internal validation, not by the GraphQL spec. When a client sends null to a
Maybe[T] field, Strawberry returns a validation error before the resolver is
called.
Full comparison:
| Type | Python | GraphQL | Absent | Null | Value |
|---|---|---|---|---|---|
str | Required | String! | ❌ Error | ❌ Error | ✅ Value |
str | None | Optional | String | ✅ None | ✅ None | ✅ Value |
strawberry.Maybe[str] | Maybe | String | ✅ None | ❌ Error | ✅ Some(value) |
strawberry.Maybe[str | None] | Maybe+Null | String | ✅ None | ✅ Some(None) | ✅ Some(value) |
Use Maybe when you need to distinguish between:
Common use cases:
Use regular optional types (str | None) when:
Always check if a Maybe field was provided before accessing its value:
# Good
if input.phone is not None:
user.phone = input.phone.value
# Bad - will raise AttributeError if phone is None
user.phone = input.phone.value
You can create helper functions to make Maybe handling cleaner:
def update_if_provided(obj, field_name: str, maybe_value):
"""Update object field only if Maybe value was provided."""
if maybe_value is not None:
setattr(obj, field_name, maybe_value.value)
# Usage
update_if_provided(user, "phone", input.phone)
update_if_provided(user, "email", input.email)
If you're currently using strawberry.UNSET to differentiate between absent and
null values, we recommend migrating to Maybe for better type safety and
clearer semantics.
Maybe provides advantages over UNSET:
Maybe[T] is a proper generic type that works correctly
with type checkers, whereas UNSET is typed as Any which defeats static
analysisUNSET, you write field: str | None = UNSET
where it's ambiguous whether None is a valid value or represents "absent".
With Maybe, Maybe[str] means null is invalid, while Maybe[str | None]
explicitly allows null as a valueBefore (using UNSET):
import strawberry
@strawberry.input
class UpdateUserInput:
name: str | None = strawberry.UNSET
phone: str | None = strawberry.UNSET
@strawberry.type
class Mutation:
@strawberry.mutation
def update_user(self, input: UpdateUserInput) -> User:
if input.name is not strawberry.UNSET:
user.name = input.name # Could be a string or None
if input.phone is not strawberry.UNSET:
user.phone = input.phone
return user
After (using Maybe):
import strawberry
@strawberry.input
class UpdateUserInput:
name: strawberry.Maybe[str | None]
phone: strawberry.Maybe[str | None]
@strawberry.type
class Mutation:
@strawberry.mutation
def update_user(self, input: UpdateUserInput) -> User:
if input.name is not None:
user.name = input.name.value # Access via .value
if input.phone is not None:
user.phone = input.phone.value
return user
| Aspect | UNSET | Maybe |
|---|---|---|
| Check absent | value is strawberry.UNSET | value is None |
| Access value | value (direct) | value.value (via Some) |
| Type annotation | T | None = UNSET | Maybe[T] or Maybe[T | None] |
| Null handling | Implicit | Explicit with T | None |
If you have existing Maybe[T] annotations that need to accept explicit null
values, Strawberry provides a codemod to convert them to Maybe[T | None]:
python -m libcst.tool codemod strawberry.codemods.maybe_optional.ConvertMaybeToOptional .
Note: This codemod is for updating Maybe annotations, not for migrating from
UNSET to Maybe. Migration from UNSET requires manual changes to update
both the type annotations and the value access patterns (from value to
value.value).