---
title: "Quickstart: Python SDK"
meta:
    description: Build Tinybird projects with the Python SDK and use the bundled CLI commands.
---

# Quickstart: Python SDK

This quickstart walks through creating a Tinybird project with the Python SDK. You define Data Sources and Endpoints in Python, use the CLI to build and deploy, and call your Endpoint from application code.

{% callout type="note" %}
Looking for other ways to start? See [Quickstarts](/forward/quickstarts).
{% /callout %}

If you use a coding agent like Cursor, Claude Code, Amp, or Open Code, install Tinybird agent skills so your agent understands Tinybird project structure and workflows:

```shell
npx skills add tinybirdco/tinybird-agent-skills
```

## Before you begin

To get started, you need:

- Python 3.11, 3.12, or 3.13
- A Tinybird Workspace
- git
- `uv` (recommended [Python package manager](https://docs.astral.sh/uv/))

{% callout type="note" %}
Python 3.14 is not supported yet for this workflow. The Python SDK relies on the Tinybird CLI under the hood, and current CLI support covers Python 3.11 to 3.13.
{% /callout %}

## Initialize a Python project and add the SDK

Create a new directory and initialize a Python SDK project:

```shell
mkdir tinybird-python-quickstart
cd tinybird-python-quickstart
git init

uv init --name tinybird-sdk-python-sample --python 3.13
uv add tinybird-sdk python-dotenv

uv run tinybird init --folder src/tinybird/
```

This creates:

- `tinybird.config.json` with an `include` path for Python resources
- `.env.local` for your Tinybird credentials

## Check your config file

In `tinybird.config.json`, `"include"` points to the folder with your Python resource definitions. The default `"devMode"` is `"branches"` to develop in Tinybird Cloud Branches, although you can use `"local"` too with Tinybird Local.

## Check your Tinybird token

Create a `.env.local` file with your Tinybird token if it doesn't exist yet:

```shell
TINYBIRD_TOKEN=p.your_token_here
TINYBIRD_URL={% user("apiHost", "https://api.tinybird.co") %}
```

The CLI reads `.env.local` automatically.

## Define resources in Python

Create `src/tinybird/tinybird_resources.py`. The schema and engine stay the same regardless of how you ingest data. The difference is how data arrives: via the Events API, from a Kafka topic, or from an S3 bucket.

### Data source

{% tabs initial="Events" %}
{% tab label="Events" %}
```python
from tinybird_sdk import define_datasource, engine, t

page_views = define_datasource(
    "page_views",
    {
        "description": "Page view tracking data",
        "schema": {
            "timestamp": t.date_time(),
            "session_id": t.string(),
            "pathname": t.string(),
            "referrer": t.string().nullable(),
        },
        "engine": engine.merge_tree(
            {
                "sorting_key": ["pathname", "timestamp"],
            }
        ),
    },
)
```
{% /tab %}
{% tab label="Kafka" %}
```python
from tinybird_sdk import define_kafka_connection, define_datasource, engine, t

my_kafka = define_kafka_connection(
    "my_kafka",
    {
        "bootstrap_servers": "your-broker:9092",
        "security_protocol": "SASL_SSL",
        "sasl_mechanism": "PLAIN",
        "key": '{{ tb_secret("KAFKA_KEY") }}',
        "secret": '{{ tb_secret("KAFKA_SECRET") }}',
    },
)

page_views = define_datasource(
    "page_views",
    {
        "description": "Page view tracking data",
        "schema": {
            "timestamp": t.date_time(),
            "session_id": t.string(),
            "pathname": t.string(),
            "referrer": t.string().nullable(),
        },
        "engine": engine.merge_tree(
            {
                "sorting_key": ["pathname", "timestamp"],
            }
        ),
        "kafka": {
            "connection": my_kafka,
            "topic": "page_views",
            "group_id": '{{ tb_secret("KAFKA_GROUP_ID") }}',
            "auto_offset_reset": "latest",
        },
    },
)
```
{% /tab %}
{% tab label="S3" %}
```python
from tinybird_sdk import define_s3_connection, define_datasource, engine, t

my_s3 = define_s3_connection(
    "my_s3",
    {
        "region": "us-east-1",
        "arn": '{{ tb_secret("S3_ARN") }}',
    },
)

page_views = define_datasource(
    "page_views",
    {
        "description": "Page view tracking data",
        "schema": {
            "timestamp": t.date_time(),
            "session_id": t.string(),
            "pathname": t.string(),
            "referrer": t.string().nullable(),
        },
        "engine": engine.merge_tree(
            {
                "sorting_key": ["pathname", "timestamp"],
            }
        ),
        "s3": {
            "connection": my_s3,
            "bucket_uri": "s3://my-bucket/page_views/*.ndjson",
            "schedule": "@auto",
        },
    },
)
```
{% /tab %}
{% /tabs %}

### Endpoint

The endpoint reads from the Data Source regardless of how data is ingested:

```python
from tinybird_sdk import define_endpoint, node, p, t

top_pages = define_endpoint(
    "top_pages",
    {
        "description": "Get the most visited pages",
        "params": {
            "start_date": p.date_time(),
            "end_date": p.date_time(),
            "limit": p.int32().optional(10),
        },
        "nodes": [
            node(
                {
                    "name": "aggregated",
                    "sql": """
                        SELECT pathname, count() AS views
                        FROM page_views
                        WHERE timestamp >= {{DateTime(start_date)}}
                          AND timestamp <= {{DateTime(end_date)}}
                        GROUP BY pathname
                        ORDER BY views DESC
                        LIMIT {{Int32(limit, 10)}}
                    """,
                }
            )
        ],
        "output": {
            "pathname": t.string(),
            "views": t.uint64(),
        },
    },
)
```

Create `src/tinybird/client.py`:

```python
import os

from tinybird_sdk import Tinybird
from src.tinybird.tinybird_resources import page_views, top_pages

tinybird = Tinybird(
    {
        "datasources": {"page_views": page_views},
        "pipes": {"top_pages": top_pages},
        "base_url": os.getenv("TINYBIRD_URL", "https://api.tinybird.co"),
        "token": os.getenv("TINYBIRD_TOKEN"),
    }
)
```

## Build the project

Commit your project files and check out a new git branch:

- `pyproject.toml` — project metadata and dependencies
- `uv.lock` — pinned dependency versions (like `package-lock.json`)
- `.python-version` — ensures consistent Python version
- `.gitignore`
- Your source code and tests

```shell
git add pyproject.toml uv.lock .python-version .gitignore src/ tinybird.config.json main.py
git commit -m "Initial commit"
git checkout -b tinybird-intro
```

Run a build to create your resources in your Tinybird development environment:

```shell
uv run tinybird build
```

#### Git integration

You may have noticed that the quickstart example had a `git checkout -b tinybird-intro`. If Tinybird detects you are in a Git repository, it creates a branch with the git branch name in your Tinybird development environment.

## Ingest data and query

### Ingest

Use the runtime client to send events via the Events API:

```python
from datetime import datetime, timezone
from src.tinybird.client import tinybird

now = datetime.now(timezone.utc).isoformat(timespec="milliseconds")

tinybird.page_views.ingest(
    {
        "timestamp": now,
        "session_id": "abc123",
        "pathname": "/home",
        "referrer": "https://google.com",
    }
)
```

{% callout type="note" %}
If you configured a Kafka or S3 connector, data is ingested automatically after you deploy. You don't need to call `ingest` manually.
{% /callout %}

### Query

Query the endpoint from your application code:

```python
result = tinybird.top_pages.query(
    {
        "start_date": "2026-01-01 00:00:00",
        "end_date": now,
        "limit": 5,
    }
)

for row in result["data"]:
    print(row["pathname"], row["views"])
```

## Iterate in a branch

Add a materialized view in another branch, test the app against that branch, and deploy to Tinybird Cloud when you're finished.

```bash
git checkout -b materialization
```

Edit `tinybird_resources.py` to add a new Data Source, a new materialized view, and update the Endpoint to read from the materialized view.

```python
from tinybird_sdk import (
    define_datasource,
    define_endpoint,
    define_materialized_view,
    engine,
    node,
    p,
    t,
)

page_views = define_datasource(
    "page_views",
    {
        "description": "Page view tracking data",
        "schema": {
            "timestamp": t.date_time(),
            "session_id": t.string(),
            "pathname": t.string(),
            "referrer": t.string().nullable(),
        },
        "engine": engine.merge_tree(
            {
                "sorting_key": ["pathname", "timestamp"],
            }
        ),
    },
)

daily_page_views_ds = define_datasource(
    "daily_page_views_ds",
    {
        "description": "Daily page view counts populated by a materialized view",
        "schema": {
            "date": t.date(),
            "pathname": t.string(),
            "views": t.simple_aggregate_function("sum", t.uint64()),
        },
        "engine": engine.aggregating_merge_tree(
            {
                "sorting_key": ["date", "pathname"],
            }
        ),
        "json_paths": False,
    },
)

daily_page_views_mv = define_materialized_view(
    "daily_page_views_mv",
    {
        "description": "Materialized rollup of page views by day and pathname",
        "datasource": daily_page_views_ds,
        "nodes": [
            node(
                {
                    "name": "aggregate",
                    "sql": """
                        SELECT
                            toDate(timestamp) AS date,
                            pathname,
                            count() AS views
                        FROM page_views
                        GROUP BY date, pathname
                    """,
                }
            )
        ],
    },
)

top_pages = define_endpoint(
    "top_pages",
    {
        "description": "Get the most visited pages from MV-backed datasource",
        "params": {
            "start_date": p.date_time(),
            "end_date": p.date_time(),
            "limit": p.int32().optional(10),
        },
        "nodes": [
            node(
                {
                    "name": "aggregated",
                    "sql": """
                        SELECT
                            pathname,
                            sum(views) AS views
                        FROM daily_page_views_ds
                        WHERE date >= toDate({{DateTime(start_date)}})
                          AND date <= toDate({{DateTime(end_date)}})
                        GROUP BY pathname
                        ORDER BY views DESC
                        LIMIT {{Int32(limit, 10)}}
                    """,
                }
            )
        ],
        "output": {
            "pathname": t.string(),
            "views": t.uint64(),
        },
    },
)
```

Build against the branch:

```bash
uv run tinybird build
```

Once validated, you can deploy to Tinybird Cloud with `uv run tinybird deploy` if you haven't set up a CI/CD script.

## Next steps

- Learn Tinybird concepts: [Core concepts](/forward/get-started/concepts)
- Review Python resource definitions: [Python SDK resources](/forward/dev-reference/python-sdk-resources)
- Review command options: [Python SDK CLI commands](/forward/dev-reference/commands/python-sdk-cli)
- Configure branches and deployments: [Development workflow](/forward/development-workflow)
- Review the SDK repository for advanced usage: [tinybird-sdk-python](https://github.com/tinybirdco/tinybird-sdk-python)
