mrkeyoor.com_
Sun 20 Sept 11:44 UTC
PyPIWeb Backendupdated 16 Sept 2026

graphql-core review

GraphQL-core 3.2.11 is the Python implementation beneath Graphene, Ariadne, Strawberry, and other GraphQL server layers. Direct users get the parser, AST visitor, schema objects, standard validation rules, query executor, subscription engine, introspection helpers, and `GraphQLError` model. HTTP routing, WebSocket protocol handling, DataLoader-style batching, and decorator-based schema authoring are outside the package. This release tracks GraphQL.js 16.14.1, supports Python 3.7 through 3.14, makes `ofType` introspection depth configurable, permits directives on directive definitions, and restores an own-property check during AST value coercion.

Verdict

GraphQL-core 3.2.11 installed as 1 package and 2 MB in 0.2 seconds on our sandbox, giving Python code direct access to the GraphQL.js 16.14.1 execution model. Use it for GraphQL infrastructure or low-level control; a normal API team will write less glue with Strawberry or Ariadne on top.

We installed it

Lab card: what happened when we installed graphql-coreScreenshot of graphql-core documentation
Install✓ · 0.2s1 package on disk · 2 MB
Importimport graphql in 0.71s · pure Python · py.typed · requires Python >=3.7,<4
Known vulns0(pip-audit)

Answers from our run

Does graphql-core install cleanly?

Yes. In a fresh container with an empty cache, pip install graphql-core finished in 0.2s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.

What does graphql-core need to run?

Python >=3.7,<4, and nothing compiled: it is pure Python. In our run import graphql succeeded in 0.71s, and the package ships py.typed for type checkers.

graphql-core or strawberry-graphql: which should you use?

strawberry-graphql: Use it for annotation-driven schemas and maintained ASGI or framework adapters. GraphQL-core 3.2.11 installed as 1 package and 2 MB in 0.2 seconds on our sandbox, giving Python code direct access to the GraphQL.js 16.14.1 execution model.

When should you not use graphql-core?

The goal is an ordinary GraphQL API with an ASGI or Django endpoint. Strawberry, Ariadne, and Graphene supply the missing schema and web integration while depending on this engine.

API stability3/5The stable unit is the 3.2 patch line, where the maintainer recommends a `~=3.2.0` constraint and 3.2.11 adds fixes without changing the main schema and execution calls. The version policy is the catch: GraphQL.js major changes arrive in a GraphQL-core minor, so 3.2 to 3.3 can break callers. The 3.3 candidate also follows GraphQL.js 17 prereleases and raises the Python floor.
Docs4/5The README explains schema construction, Python resolver arguments, synchronous and asyncio execution, version pinning, and the relationship to Graphene, Ariadne, and Strawberry. Read the Docs adds sections for the language, types, validation, execution, utilities, and API reference. Some specification details still point readers toward GraphQL.js material, where resolver signatures and occasional port behavior differ from Python.
Maintenance4/5Version 3.2.11 shipped on 2026-06-05, the repository was pushed on 2026-07-15, and GitHub reports 30 open issues and pull requests. The release follows GraphQL.js 16.14.1 and adds Python 3.14 support alongside focused coercion and introspection fixes. The README says the port mirrors more than 3,000 reference tests, though it also identifies one primary maintainer, which leaves some continuity risk.
Ecosystem5/5The supplied registry count is 19,584,372 weekly downloads, and GitHub reports 529 stars. Direct popularity is less informative here because Strawberry, Ariadne, and Graphene 3 use GraphQL-core underneath. Its schema classes, parser, validation rules, executor, introspection helpers, and errors therefore form a shared compatibility layer across much of Python GraphQL, even when application developers never import it themselves.

Use it if

  • You are writing a query analyzer, persisted-query service, custom validation rule, schema tool, or another layer that needs the GraphQL machinery directly.
  • A framework abstraction is hiding resolver context, execution middleware, AST nodes, or schema details that your service must control.
  • Python behavior should stay close to the GraphQL.js reference implementation for parsing, coercion, validation, and execution.
  • The application needs to construct `GraphQLSchema` objects or consume `subscribe()` results without adopting a schema framework.
Skip it if

Setup reality

We installed GraphQL-core 3.2.11 in a clean Python 3.12 Bookworm container. The job took 0.2 seconds and left 1 package using 2 MB. pip-audit found 0 known vulnerabilities, and import graphql completed in 0.71 seconds. It is pure Python, declares 1 direct dependency, includes py.typed, and accepts Python 3.7 or newer below Python 4. No compiler, account, service, or configuration file was involved.

Pin the 3.2 line deliberately. GraphQL-core does not copy GraphQL.js version numbers: a GraphQL.js major becomes a GraphQL-core minor, so 3.3 can break code that works on 3.2. The README recommends ~=3.2.0. Release 3.2.11 corresponds to GraphQL.js 16.14.1, while the 3.3 release candidate follows GraphQL.js 17 prereleases and starts at Python 3.10. Check a framework's constraint before forcing a newer transitive version.

Direct schema construction is explicit. Fields use GraphQLField, arguments use GraphQLArgument, and SDL-built fields still need Python resolvers attached. A Python resolver receives parent and GraphQLResolveInfo positionally, then GraphQL field arguments as keywords. JavaScript examples use a different calling convention. Put per-request database sessions and identity on info.context. Call graphql_sync only when every resolver is synchronous; use and await graphql when a resolver can return a coroutine.

The convenience entry points parse, validate, and execute on each call. Persisted-query systems can cache a parsed document after validation, then call execute, but a schema revision must invalidate that cache. subscribe() yields an async iterator after successful setup and an ExecutionResult on setup error. Resolver exceptions become GraphQLError instances and retain original_error for logs. Filter client-facing error messages because exception text may contain internal data.

Patterns

Build an SDL schema and attach a resolver build-schema-sdl

from graphql import build_schema

schema = build_schema("""
  type Query { greeting(name: String!): String! }
""")

def greeting(_parent, _info, name: str) -> str:
    return f"Hello, {name}"

schema.query_type.fields["greeting"].resolve = greeting

`build_schema` creates types and fields but does not discover Python functions; each application resolver still needs binding.

Declare schema objects directly construct-object-schema

from graphql import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString

query = GraphQLObjectType(
    "Query",
    {"health": GraphQLField(GraphQLString, resolve=lambda _p, _i: "ok")},
)
schema = GraphQLSchema(query=query)

Direct construction requires `GraphQLField` objects rather than plain resolver callables.

Execute a synchronous query run-sync-query

from graphql import graphql_sync

result = graphql_sync(schema, '{ greeting(name: "Ada") }')
if result.errors:
    raise ValueError([error.message for error in result.errors])
print(result.data)

`graphql_sync` parses and validates before execution, and every resolver in that path must return a non-awaitable value.

Execute coroutine resolvers run-async-query

from graphql import graphql

async def account(_parent, info, id: str):
    return await info.context["accounts"].get(id)

schema.query_type.fields["account"].resolve = account
result = await graphql(schema, source, context_value={"accounts": store})

Request services passed through `context_value` appear on `info.context`; await `graphql` when any resolver may return a coroutine.

Parse, validate, then execute separate-execution-stages

from graphql import execute, parse, validate

document = parse(source)
errors = validate(schema, document)
if errors:
    raise ValueError([item.message for item in errors])
result = execute(schema, document, context_value=context)

Cached documents must be revalidated when the schema changes; otherwise removed fields or rules can slip past the cache.

Add a field-level validation rule define-validation-rule

from graphql import GraphQLError, ValidationRule, parse, specified_rules, validate

class RejectSecret(ValidationRule):
    def enter_field(self, node, *_args):
        if node.name.value == "secret":
            self.report_error(GraphQLError("Field unavailable", node))

errors = validate(schema, parse(source), (*specified_rules, RejectSecret))

Keep `specified_rules` in the tuple. Passing only the custom class disables the standard GraphQL validation set.

Reject schema introspection fields disable-introspection

from graphql import NoSchemaIntrospectionCustomRule, parse, specified_rules, validate

rules = (*specified_rules, NoSchemaIntrospectionCustomRule)
errors = validate(schema, parse(source), rules)

This custom rule blocks `__schema` and `__type` but keeps `__typename`; it does not replace authorization checks on ordinary fields.

Collect selected field names walk-query-ast

from graphql import Visitor, parse, visit

class Fields(Visitor):
    def __init__(self):
        super().__init__()
        self.names = []

    def enter_field(self, node, *_args):
        self.names.append(node.name.value)

collector = Fields()
visit(parse(source), collector)

Visitor callbacks use snake-case names such as `enter_field`, and subclasses must call the base initializer.

Return GraphQL-shaped data and errors format-errors

result = graphql_sync(schema, source, context_value=context)
payload = result.formatted

for error in result.errors or []:
    logger.error("resolver error", exc_info=error.original_error)

`formatted` is suitable for the response shape, but raw exception messages may expose internal details and should be filtered by the server.

Recreate a schema from introspection build-client-schema

from graphql import build_client_schema, get_introspection_query, graphql_sync

result = graphql_sync(schema, get_introspection_query())
if result.errors:
    raise RuntimeError(result.errors)
client_schema = build_client_schema(result.data)

An introspected client schema contains type information but no executable field resolvers.

Apply an SDL extension extend-schema

from graphql import extend_schema, parse

extension = parse('extend type Query { health: String! }')
schema = extend_schema(schema, extension)

`extend_schema` returns a new schema. Attach a resolver to each newly introduced executable field.

Handle subscription setup and events consume-subscription

from graphql import ExecutionResult, parse, subscribe

stream = await subscribe(schema, parse(source), context_value=context)
if isinstance(stream, ExecutionResult):
    raise ValueError(stream.errors)
async for event in stream:
    await socket.send_json(event.formatted)

Setup errors return `ExecutionResult`; successful setup returns an async iterator. WebSocket protocol handling remains the server's job.

Alternatives

PackageRegistryPick it when
strawberry-graphqlPyPIUse it for annotation-driven schemas and maintained ASGI or framework adapters.
ariadnePyPIUse it when SDL is authoritative and resolvers should bind onto named schema fields.
graphenePyPIUse it for class-based schemas or an existing graphene-django codebase.

More web backend guides

urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.