- Python 100%
| .github/workflows | ||
| graphql_client_generator | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
GraphQL Client Generator
Generate typed, tab-completable Python clients from GraphQL schema files.
Warning: This project is in early beta. APIs may change without notice, edge cases abound, and there are dragons. Use in production at your own risk.
Installation
pip install graphql-client-generator
Requires Python >= 3.10.
Quick Start
Given a schema like bookstore.graphqls:
type Query {
book(id: ID!): Book
books(limit: Int = 10): [Book!]!
genres: [Genre!]!
}
type Mutation {
addBook(input: AddBookInput!): Book!
}
type Book {
id: ID!
title: String!
isbn: String
pageCount: Int
author: Author!
reviews: [Review!]!
}
type Author {
id: ID!
name: String!
books: [Book!]!
}
type Review {
id: ID!
rating: Int!
text: String
}
type Genre {
id: ID!
name: String!
description: String
}
input AddBookInput {
title: String!
authorId: ID!
isbn: String
}
Generate a client from a local schema file
python -m graphql_client_generator bookstore.graphqls
This produces a standalone Python package (bookstore/) with typed models,
a client class, and a query builder.
To embed the generated client inside an existing package instead, pass
--module to skip the pyproject.toml:
python -m graphql_client_generator bookstore.graphqls --module
Generate a client from a live endpoint
Pass an http:// or https:// URL instead of a file path and the schema is
fetched automatically via GraphQL introspection:
python -m graphql_client_generator https://api.example.com/graphql -n bookstore
Use -H (repeatable) to add HTTP headers - useful for authenticated endpoints:
python -m graphql_client_generator https://api.example.com/graphql \
-H "Authorization: Bearer $TOKEN" \
-n bookstore
Generate from a notebook or script
generate_from_endpoint lets you pass a requests.Session directly, so any
auth, cookies, or TLS settings you have already configured are reused:
import requests
import graphql_client_generator as gcg
session = requests.Session()
session.headers["Authorization"] = "Bearer <token>"
gcg.generate_from_endpoint(
"https://api.example.com/graphql",
name="bookstore",
session=session,
# as_package=False # omit pyproject.toml when embedding in an existing package
)
CLI options
| Flag | Description | Default |
|---|---|---|
-n, --name |
Package name | Schema filename stem (file) or client (URL) |
-o, --output |
Output directory | Current directory |
--module |
Emit Python files only, no pyproject.toml |
Off |
-H, --header |
HTTP header for introspection (Name: Value), repeatable |
Use the generated client
from bookstore import BookstoreClient, BookstoreQuery, BookstoreMutation, Variable
from bookstore.outputs import Book, Author
client = BookstoreClient("https://api.example.com/graphql")
Query Builder
The generated BookstoreQuery object provides a typed, tab-completable
interface for building GraphQL queries. Every field from the schema is
available as an attribute with full IDE support.
Syntax
| Syntax | Meaning |
|---|---|
Query.field |
Select a field |
Query.field(arg=val) |
Pass arguments to a field |
selector[field1, field2] |
Select sub-fields (tuple, list, or any iterable) |
selector[lambda s: (s.a, s.b)] |
Lambda shorthand for sub-field selection |
selector.as_("alias") |
Alias a field |
selector.ALL |
Recursively select all fields without required args |
selector.ALL_SCALAR |
Select only scalar fields (default when no sub-fields given) |
selector.ALL_SHALLOW |
Select all fields one level deep (composites get scalar children) |
Variable.name |
Reference a query variable |
Query[...] |
Build a complete query |
Examples
Basic query:
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=42)[
Book.title,
Book.isbn,
],
],
)
print(result.book.title)
Book.title and BookstoreQuery.book.title are equivalent -- use whichever
reads better in context.
Multiple root fields:
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=42)[Book.title, Book.isbn],
BookstoreQuery.genres, # auto-selects all scalar sub-fields
],
)
Aliases:
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=42)[
Book.title,
Book.reviews.as_("recent_reviews")[
Review.rating,
Review.text,
],
],
],
)
print(result.book.recent_reviews)
Query variables:
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=Variable.book_id)[
Book.title,
Book.author[Author.name],
],
],
variables={"book_id": 42},
)
Variable types are automatically inferred from the schema and included in the
generated query string (e.g. query($book_id: ID!) { ... }).
Raw GraphQL strings are still supported:
result = client.query(
'{ book(id: 42) { title author { name } } }',
)
Tab Completion
In Jupyter notebooks and IPython, pressing <Tab> after BookstoreQuery. or
BookstoreQuery.book. lists all available fields. dir() on any
FieldSelector returns its child fields.
Argument Validation
Fields that accept arguments have those arguments defined in the schema. If
you pass an unknown argument, a TypeError is raised immediately:
BookstoreQuery.book(bad_arg=1)
# TypeError: Unknown argument 'bad_arg' for field 'book'. Valid arguments: id
Fields that take no arguments raise TypeError if called:
Book.title(x=1)
# TypeError: Field 'title' takes no arguments
Flat Path Syntax
Deep dotted paths in sub-field selections are automatically nested. Instead of
manually wrapping each level with []:
BookstoreQuery[
BookstoreQuery.book(id=42)[
Book.title,
Book.author[Author.name, Author.books[Book.title]],
],
]
You can write the paths flat:
Q = BookstoreQuery
BookstoreQuery[
Q.book(id=42)[
Q.book.title,
Q.book.author.name,
Q.book.author.books.title,
],
]
Paths through the same intermediate field are merged automatically
(author.name and author.books.title become author { name books { title } }).
Selections are validated: using a path that doesn't match the receiver raises
TypeError.
Lambda Shorthand
A callable passed to [] receives the field selector itself, avoiding the need
to repeat the prefix:
BookstoreQuery[
BookstoreQuery.book(id=42)[lambda b: (
b.title,
b.author.name,
b.author.books.title,
)],
]
The lambda (or any callable) can return a single selector, a tuple, a list, or any iterable.
Expansion Modes
When a composite-type field is used without explicit sub-field selections, its
scalar fields are automatically included (ALL_SCALAR mode):
BookstoreQuery.genres
# Equivalent to:
BookstoreQuery.genres[Genre.id, Genre.name, Genre.description]
Three explicit expansion modes are available on any composite field:
| Mode | Behavior |
|---|---|
.ALL_SCALAR |
Scalar fields only (default) |
.ALL |
Recursively expand all fields that don't require arguments |
.ALL_SHALLOW |
All fields one level deep; composite children get scalar-only expansion |
# Fetch everything recursively (skips fields that require arguments)
BookstoreQuery[BookstoreQuery.book(id=42).ALL]
# Fetch scalars only
BookstoreQuery[BookstoreQuery.book(id=42).ALL_SCALAR]
# Fetch one level: scalars + immediate composites (with their scalars)
BookstoreQuery[BookstoreQuery.book(id=42).ALL_SHALLOW]
Response Objects
Query results are wrapped in GraphQLResponse objects that provide typed
attribute access. Field names are automatically converted to snake_case.
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=42)[
Book.title,
Book.page_count,
Book.author[Author.id, Author.name],
],
],
)
result.book.title # str
result.book.page_count # int
result.book.author # GraphQLResponse (typed as Author)
result.book.author.name # str
Repr
Response objects display with their schema type name and loaded fields:
>>> result.book
Book(title='The Great Gatsby', page_count=180, author=Author(id=7, name='F. Scott Fitzgerald'))
Long reprs automatically wrap with indentation at 80 characters.
Serialization
result.book.to_dict() # -> {"title": "The Great Gatsby", "pageCount": 180, ...}
result.book.to_json() # -> '{"title": "The Great Gatsby", "pageCount": 180, ...}'
Lazy Loading
If auto_fetch is enabled (the default), accessing a field that was not
included in the original query triggers an automatic re-query:
result = client.query(
BookstoreQuery[
BookstoreQuery.book(id=42)[Book.title],
],
)
# 'author' was not in the query, but accessing it triggers a lazy fetch:
result.book.author # automatically re-queries the server
The lazy loader:
- Finds the field's type from the schema metadata
- Determines which scalar sub-fields to request
- Modifies the original query to include the missing field
- Re-executes the query and extracts the new data
- Caches the result for subsequent access
Disable with auto_fetch=False:
client = BookstoreClient("...", auto_fetch=False)
Accessing an unloaded field then raises FieldNotLoadedError.
Mutations
The generated BookstoreMutation object works the same way as BookstoreQuery.
When a mutation field takes a single Input type, its fields are flattened into
keyword arguments -- no need to construct the Input object manually:
result = client.mutate(
BookstoreMutation[
BookstoreMutation.add_book(title="New Book", author_id="123")[
Book.id,
Book.title,
],
],
)
print(result.add_book.title)
The keyword arguments are forwarded to the AddBookInput constructor for
validation, then serialized as the input argument in the GraphQL request.
Raw GraphQL strings are also accepted:
result = client.mutate(
'mutation { addBook(input: { title: "New Book", authorId: "123" }) { id title } }',
)
Generated Package Structure
bookstore/ # Project root (dist name, hyphens)
bookstore/ # Python module (import name, underscores)
__init__.py # Exports client, query builder, enums, inputs, outputs
client.py # BookstoreClient class
schema.py # BookstoreQuery and BookstoreMutation query builders
outputs.py # Schema types (Book, Author, ...) with SchemaField descriptors
enums.py # Python Enum classes
inputs.py # @dataclass input types with to_dict()
_runtime/ # Standalone runtime library
builder.py # Query builder (Variable, FieldSelector, SchemaField)
model.py # GraphQLResponse, lazy loading
client.py # GraphQLClientBase, HTTP transport
query.py # __typename insertion, query modification
serialization.py # Case conversion, input serialization
pyproject.toml # Package metadata
With --module the pyproject.toml is omitted and the source files are written
directly into bookstore/ (no nesting), ready to drop into an existing package.
Schema Support
| GraphQL Feature | Python Representation |
|---|---|
| Object types | GraphQLModel subclass with SchemaField descriptors |
| Interfaces | Base class; implementing types inherit from it |
| Unions | Type alias (A | B | C) |
| Enums | enum.Enum subclass |
| Input types | @dataclass with to_dict() |
| Custom scalars | Mapped to Python types (DateTime -> str, JSON -> Any) |
@oneOf inputs |
Validated in __post_init__ (exactly one field set) |
__typename |
Automatically inserted into all queries |
Type Mapping
| GraphQL | Python |
|---|---|
String |
str |
Int |
int |
Float |
float |
Boolean |
bool |
ID |
str |
DateTime |
str |
JSON |
Any |
[Type] |
list[Type] |
Type! |
Non-null |
Type |
Type | None |