mrkeyoor.com_
Tue 22 Sept 00:44 UTC
PyPIDataupdated 21 Sept 2026

supabase review

supabase 2.31.0 is the official Python client bundle for Supabase database queries, authentication, object storage, Edge Functions, and Realtime. Its table chains call PostgREST over HTTP, so exposed schemas and row-level security shape every result; this is not a PostgreSQL wire driver. The release changes X-Client-Info to structured semicolon-delimited metadata and makes PyIceberg an optional storage extra. The client does not offer arbitrary SQL or transactions spanning several calls, and its APIs tie the application to Supabase services.

Verdict

Our supabase 2.31.0 install took 0.8 seconds, used 30 MB across 31 packages, printed one deprecation warning, and imported in 0.80 seconds with no audit findings. Install the bundle for an existing Supabase architecture; use PostgREST alone or a PostgreSQL driver when data access is the only job.

We installed it

Lab card: what happened when we installed supabaseScreenshot of supabase documentation
Install✓ · 0.8s31 packages on disk · 30 MB · 1 deprecation warning
Importimport supabase in 0.80s · pure Python · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does supabase install cleanly?

Yes. In a fresh container with an empty cache, pip install supabase finished in 0.8s, leaving 31 packages and 30 MB on disk. pip-audit reported no known vulnerabilities. The install printed 1 deprecation warning.

What does supabase need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import supabase succeeded in 0.80s, and the package ships py.typed for type checkers.

supabase or postgrest: which should you use?

postgrest: Use it when only table and RPC requests are needed, without the auth, storage, functions, and Realtime bundle. Our supabase 2.31.0 install took 0.8 seconds, used 30 MB across 31 packages, printed one deprecation warning, and imported in 0.80 seconds with no audit findings.

When should you not use supabase?

The workload needs multi-statement transactions, CTEs, window functions, COPY, or unrestricted SQL; use an RPC function or a PostgreSQL driver

API stability4/5The 2.x line preserves create_client(), table query builders, auth, storage, functions, and parallel sync and async client classes. Version 2.31.0 changes client-identification metadata and an optional dependency rather than those calls. The metapackage pins five sibling services to the exact same release, so a fix in one arrives with all of them. Top-level imports are safer than depending on internal client classes that have moved in earlier 2.x work.
Docs3/5The official Python reference returns HTTP 200 and covers filters, writes, auth flows, storage, Edge Functions, and Realtime with Python examples. The package README gets a client running and mentions explicit sign-out during shutdown. Guidance is thinner around shared mutable auth state, dependency-pin conflicts, schema exposure, lifecycle cleanup, direct SQL limits, and the sync Realtime gap. Developers often need package READMEs and the JavaScript-oriented product docs to fill those holes.
Maintenance5/5GitHub records a push on August 24, 2026, and shows an unarchived repository with 2,569 stars and 52 open issues and pull requests combined. Release 2.31.0 shipped June 4, 2026, following regular releases across PostgREST, auth, storage, Realtime, and functions. The monorepo tests the six Python distributions together, which keeps service protocol versions coordinated even though exact sibling pins make upgrades move as a set.
Ecosystem4/5The supplied weekly download count is 8,048,489. This is Supabase's official Python entry point and it mirrors the project's database, auth, storage, functions, and Realtime services, while individual sibling packages remain installable. Python has fewer recipes and extensions than supabase-js, and the APIs stay vendor-specific. Psycopg and SQLAlchemy still have broader choices for SQL, migrations, analytics, pooling, and backend portability.

Use it if

  • The application already runs on Supabase and needs table queries, auth, storage, and functions behind one Python client
  • HTTP data access fits a short-lived service better than owning PostgreSQL connections and a pool
  • User access tokens should reach PostgREST so database row-level security remains the authorization boundary
  • Async Python code needs Realtime channels alongside the same account and project configuration
Skip it if

Setup reality

We installed supabase 2.31.0 in a fresh unprivileged Python 3.12 Bookworm sandbox in 0.8 seconds. The installer printed one deprecation warning and left 31 packages using 30 MB. The package declares 8 direct dependencies, requires Python 3.9 or newer, and is pure Python. It ships py.typed. import supabase succeeded in 0.80 seconds, and pip-audit reported zero known vulnerabilities. Our installed metadata did not identify a license value.

Creating a client requires the project URL and a key. Publishable or anon keys depend on row-level security for authorization. A service-role key bypasses those policies and belongs only in trusted server secret storage. The Python library cannot prevent a deployment from exposing it. Queries against a non-public schema also fail until that schema is added to the project's exposed-schema configuration. Test every policy with the same key and user-token path production will use.

Version 2.31.0 pins postgrest, supabase-auth, storage3, supabase-functions, and realtime to matching 2.31.0 releases and keeps httpx below 0.29. Existing httpx constraints can therefore block the full upgrade. PyIceberg moved behind supabase[iceberg], so the base install no longer supplies it. Async code should create an AsyncClient with acreate_client or create_async_client; sync and async calls belong to different objects, and Realtime uses the async route.

A query chain sends nothing until execute() and returns a response object rather than a plain list. Client auth state is mutable, so sharing one signed-in instance across unrelated web requests can leak a user context into later calls. Prefer per-request user clients or set the token deliberately. The package README calls for client.auth.sign_out() during proper shutdown. Scripts that do not need persisted sessions should disable persistence and automatic refresh in client options and close their client lifecycle.

Patterns

Create a trusted server client create-client

import os
from supabase import Client, create_client

supabase: Client = create_client(
    os.environ['SUPABASE_URL'],
    os.environ['SUPABASE_KEY'],
)

A service-role key bypasses row-level security and must never reach a browser or other untrusted client.

Filter and page table rows select-filter-rows

response = (
    supabase.table('orders')
    .select('id,total,customer:customers(name)', count='exact')
    .gte('total', 100)
    .in_('status', ['paid', 'shipped'])
    .order('created_at', desc=True)
    .range(0, 24)
    .execute()
)
rows = response.data
total = response.count

range(0, 24) has inclusive bounds and may return 25 rows; an exact count can add database work.

Require a single record fetch-one-row

profile = (
    supabase.table('profiles')
    .select('*')
    .eq('id', user_id)
    .single()
    .execute()
    .data
)

single() errors on zero or multiple matches; maybe_single() is the suitable path when absence is valid.

Insert a batch in one call insert-batch

response = supabase.table('countries').insert([
    {'code': 'FR', 'name': 'France'},
    {'code': 'ES', 'name': 'Spain'},
]).execute()

A list avoids one HTTP request per row, but large imports are better split or sent through a direct COPY workflow.

Upsert on a unique constraint upsert-conflict

response = supabase.table('countries').upsert(
    {'code': 'GB', 'capital_city': 'London'},
    on_conflict='code',
).execute()

The on_conflict column must have a matching PostgreSQL unique or exclusion constraint.

Update only filtered rows update-with-filter

response = (
    supabase.table('countries')
    .update({'capital_city': 'Jakarta'})
    .eq('id', 1)
    .execute()
)

The client allows an unfiltered update, so policy tests and review must protect whole-table mutations.

Run atomic work through RPC call-rpc

response = supabase.rpc('transfer_credits', {
    'from_user': sender_id,
    'to_user': recipient_id,
    'amount': 25,
}).execute()

Multi-table atomicity belongs inside the PostgreSQL function, with suitable schema exposure and execute grants.

Build an async client create-async-client

from supabase import AsyncClient, acreate_client

supabase: AsyncClient = await acreate_client(url, key)
response = await supabase.table('countries').select('*').execute()

Sync and async clients are separate objects, and Realtime subscriptions use the async implementation.

Apply a user session before querying authenticate-user

session = supabase.auth.sign_in_with_password({
    'email': email,
    'password': password,
})
notes = supabase.table('notes').select('*').execute().data

Signing in mutates the client token used by following calls; do not share that user client across unrelated requests.

Upload and sign a private object upload-storage-file

bucket = supabase.storage.from_('documents')
bucket.upload(
    'user-42/report.pdf',
    pdf_bytes,
    {'content-type': 'application/pdf', 'upsert': 'false'},
)
link = bucket.create_signed_url('user-42/report.pdf', 3600)

The method is from_ because from is reserved in Python; private buckets also need storage policies and expiring links.

Report an Edge Function failure invoke-edge-function

from supabase import FunctionsHttpError, FunctionsRelayError

try:
    result = supabase.functions.invoke(
        'make-report',
        invoke_options={'body': {'report_id': 42}},
    )
except (FunctionsHttpError, FunctionsRelayError) as error:
    detail = error.to_dict()
    raise RuntimeError(detail.get('message')) from error

Non-success calls raise library exceptions whose structured detail is exposed through to_dict().

Subscribe to inserted rows subscribe-realtime

supabase = await acreate_client(url, key)

def handle(payload):
    print(payload['new'])

channel = supabase.channel('messages-live')
channel.on_postgres_changes(
    event='INSERT',
    schema='public',
    table='messages',
    callback=handle,
)
await channel.subscribe()

The table must be enabled in the Supabase Realtime publication, and this channel path requires the async client.

Alternatives

PackageRegistryPick it when
postgrestPyPIUse it when only table and RPC requests are needed, without the auth, storage, functions, and Realtime bundle
psycopgPyPIUse it for direct PostgreSQL SQL, transactions, COPY, pooling, and protocol-level behavior
SQLAlchemyPyPIUse it when models, database portability, and an Alembic migration workflow matter more than Supabase integration

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · 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.