## File: README.md

**data load tool (dlt) — the open-source Python library that automates all your tedious data loading tasks**

🚀 Join our thriving community of likeminded developers and build the future together!

## Installation dlt supports Python 3.10 through Python 3.14. Note that some optional extras are not yet available for Python 3.14, so support for this version is considered experimental. ```sh pip install dlt ``` Add the extras you need for your sources and destinations, for example: ```sh pip install "dlt[duckdb]" # local DuckDB destination pip install "dlt[bigquery]" # or snowflake, postgres, redshift, databricks, athena, ... pip install "dlt[s3]" # or gs, az for cloud filesystems pip install "dlt[sql_database]" # read from any SQL database pip install "dlt[hub]" # data quality, transformations, and AI (see below) ``` Prefer [uv](https://docs.astral.sh/uv/)? `uv add "dlt[duckdb]"`. ## Quick Start Describe an API declaratively and load it into DuckDB — dlt handles requests, pagination, schema inference, and typing for you: ```python import dlt from dlt.sources.rest_api import rest_api_source # 1. Describe the API declaratively source = rest_api_source({ "client": {"base_url": "https://pokeapi.co/api/v2/"}, "resources": [ {"name": "pokemon", "endpoint": {"path": "pokemon", "params": {"limit": 1000}}}, ], }) # 2. Point a pipeline at any destination pipeline = dlt.pipeline( pipeline_name="pokemon", destination="duckdb", dataset_name="pokemon_data", ) # 3. Extract, normalize, and load print(pipeline.run(source)) # 4. ...and read it straight back as a DataFrame print(pipeline.dataset().pokemon.df()) ``` ...or load any Python iterable — a [resource](https://dlthub.com/docs/general-usage/resource) is just a generator, and dlt infers the schema, types the columns, and writes the table: ```python import dlt @dlt.resource(table_name="players", primary_key="id", write_disposition="merge") def players(): yield {"id": 1, "name": "Magnus", "rating": 2839} yield {"id": 2, "name": "Pragg", "rating": 2758} dlt.pipeline(destination="duckdb", dataset_name="chess").run(players()) ``` Check out a super simple demo in **[Colab](https://colab.research.google.com/drive/1NfSB1DpwbbHX9_t5vlalBTf13utwpMGx?usp=sharing)** or a more advanced [Hugging Face demo with Marimo notebooks](https://molab.marimo.io/github/marimo-team/gallery-examples/blob/main/notebooks/external/dlthub-huggingface.py). ## Why dlt **dlt** loads data from messy, often unstructured sources into well-structured, typed datasets. It's a **library, not a platform** — you `pip install` it into your existing code and keep your workflow and the other tools you already use. No black boxes: clean Pythonic interfaces, human-readable file formats, schemas you can inspect, no hidden side effects. dlt and its docs are **built from the ground up for LLMs and coding agents**. Pair the typed, declarative primitives below with [dlthub.com/context](https://dlthub.com/context) and the [LLM-native workflow](https://dlthub.com/docs/dlt-ecosystem/llm-tooling/llm-native-workflow) to go from prompt to working pipeline — across [5000+ sources](https://dlthub.com/workspace) — often in a single shot. ## Extract from any source **REST APIs** — describe the endpoints declaratively; filter, map, and flatten records right at the source ([docs](https://dlthub.com/docs/tutorial/rest-api)): ```python from dlt.sources.rest_api import rest_api_source source = rest_api_source({ "client": { "base_url": "https://api.example.com/v1", "paginator": {"type": "cursor", "cursor_path": "next_cursor"}, }, "resources": [ { "name": "guests", "endpoint": {"path": "events/guests"}, "processing_steps": [ {"filter": lambda r: r["approval_status"] == "approved"}, {"map": lambda r: {**r, "email": r["email"].lower()}}, ], }, ], }) ``` **SQL databases** — reflect tables and types straight from the database ([docs](https://dlthub.com/docs/tutorial/sql-database)): ```python from dlt.sources.sql_database import sql_database source = sql_database("mysql+pymysql://user:pass@host/db") ``` **Files in any bucket** — list, then parse CSV / JSONL / Parquet from local disk, S3, GCS, or Azure ([docs](https://dlthub.com/docs/tutorial/filesystem)): ```python from dlt.sources.filesystem import filesystem, read_csv_duckdb source = ( filesystem(bucket_url="s3://my-bucket/data", file_glob="*.csv") | read_csv_duckdb() ).with_name("events") ``` **DataFrames & Arrow** — pandas, Polars, and Arrow tables load directly; Arrow-backed frames move with zero copies: ```python import dlt import pandas as pd df = pd.DataFrame({"event": ["dlt summit", "DuckCon"], "signups": [1240, 860]}) dlt.pipeline(destination="duckdb", dataset_name="events").run(df, table_name="events") ``` See [many more sources](https://dlthub.com/docs/dlt-ecosystem/verified-sources) in the ecosystem. ## Load to 20+ destinations — swap one string The same resource runs anywhere. Change the `destination` string and dlt takes care of credentials, DDL in the target dialect, staging, and schema drift: ```python pipeline = dlt.pipeline( pipeline_name="luma", destination="duckdb", # → snowflake, bigquery, postgres, redshift, databricks, dataset_name="luma_data", # athena, clickhouse, motherduck, filesystem (S3/GCS/Azure), ) # iceberg, delta, ... and custom reverse-ETL destinations pipeline.run(source) ``` dlt handles the parts you'd rather not: - **Credentials** → `secrets.toml` / env vars, injected automatically - **DDL** → `CREATE TABLE` in the target's dialect - **Type mapping** → source types converted to the destination's types - **Staging** → S3 / GCS for warehouses that need it - **Schema drift** → `ALTER TABLE` on the fly Browse all [supported destinations](https://dlthub.com/docs/dlt-ecosystem/destinations/), or build a [custom one](https://dlthub.com/docs/dlt-ecosystem/destinations/destination). ## Declare intent with decorators Decorators let you declare *what* you want — incremental loading, merge strategies, schema contracts, column hints — instead of hand-rolling it. Every knob can be overridden at runtime ([docs](https://dlthub.com/docs/general-usage/resource)): ```python import dlt @dlt.resource( primary_key="id", write_disposition="merge", # upsert on the primary key columns={"email": {"x-annotation-pii": True}}, # type and annotate columns schema_contract={"columns": "freeze"}, # reject unexpected columns ) def events( updated_at=dlt.sources.incremental("updated_at"), # load only new/changed rows ): yield from fetch_events(since=updated_at.last_value) @dlt.source def luma(api_key: str = dlt.secrets.value): return events(), guests() # group one or more resources behind shared config/auth ``` [**Schema contracts**](https://dlthub.com/docs/general-usage/schema-contracts) enforce the shape at the gate, with three modes — `evolve` (accept and adapt the schema), `freeze` (reject the record), and `discard` (drop the offending row/column) — applied independently to `tables`, `columns`, and `data_type`. You also get [schema inference](https://dlthub.com/docs/general-usage/schema), [normalization of nested data](https://dlthub.com/docs/general-usage/schema/#data-normalizer), [incremental loading](https://dlthub.com/docs/general-usage/incremental-loading), and [secrets & config injection](https://dlthub.com/docs/general-usage/credentials) out of the box. ## Read your data back: the Dataset API A pipeline is durable. Reconnect to one by name with `dlt.attach` and read any table back in the shape that fits your tool ([docs](https://dlthub.com/docs/general-usage/dataset-access/)): ```python import dlt pipeline = dlt.attach(pipeline_name="luma", destination="duckdb", dataset_name="luma_data") dataset = pipeline.dataset() dataset.tables # ['events', 'guests', ...] guests = dataset.guests # a lazy dlt.Relation guests.df() # pandas DataFrame guests.arrow() # pyarrow.Table (zero-copy) guests.to_ibis() # ibis expression — lazy, composable ``` ## Transform with Ibis — Python in, SQL out Lift any loaded table into an [Ibis](https://ibis-project.org/) expression, compose group-bys, joins, and window functions in Python, and let dlt compile it to SQL in the destination's dialect. Nothing runs until you ask for the result: ```python import ibis guests = pipeline.dataset().guests.to_ibis() guests_by_event = ( guests .group_by("event_id") .aggregate(n_guests=ibis._.api_id.count()) ) guests_by_event.to_pyarrow() # compiles to SQL and runs on the destination ``` dlt also supports [Python and SQL data access](https://dlthub.com/docs/general-usage/dataset-access/), [transformations](https://dlthub.com/docs/dlt-ecosystem/transformations), [pipeline inspection](https://dlthub.com/docs/general-usage/dashboard), and [visualizing data in Marimo notebooks](https://dlthub.com/docs/general-usage/dataset-access/marimo). ## Documentation For detailed usage and configuration, please refer to the [official documentation](https://dlthub.com/docs). ## Examples You can find examples for various use cases in the [examples](docs/examples) folder, or in the [code examples section](https://dlthub.com/docs/examples) of our docs page. ## Adding as dependency `dlt` follows the semantic versioning with the [`MAJOR.MINOR.PATCH`](https://peps.python.org/pep-0440/#semantic-versioning) pattern. * `major` means breaking changes and removed deprecations * `minor` new features, sometimes automatic migrations * `patch` bug fixes We suggest that you allow only `patch` level updates automatically using the [Compatible Release Specifier](https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release). For example **dlt~=1.23.0** allows only versions **>=1.23.0** and less than **<1.24.0** Please also see our [release notes](https://github.com/dlt-hub/dlt/releases) for notable changes between versions. ## Get Involved The dlt project is quickly growing, and we're excited to have you join our community! Here's how you can get involved: - **Connect with the Community**: Join other dlt users and contributors on our [Slack](https://dlthub.com/community) - **Report issues and suggest features**: Please use the [GitHub Issues](https://github.com/dlt-hub/dlt/issues) to report bugs or suggest new features. Before creating a new issue, make sure to search the tracker for possible duplicates and add a comment if you find one. - **Track progress of our work and our plans**: Please check out our [public Github project](https://github.com/orgs/dlt-hub/projects/9) - **Improve documentation**: Help us enhance the dlt documentation. ## Contribute code Please read [CONTRIBUTING](CONTRIBUTING.md) before you make a PR. - 📣 **New destinations are unlikely to be merged** due to high maintenance cost (but we are happy to improve SQLAlchemy destination to handle more dialects) - Significant changes require tests and docs and in many cases writing tests will be more laborious than writing code - Bugfixes and improvements are welcome! You'll get help with writing tests and docs + a decent review. ## Sponsors [Blacksmith](https://blacksmith.sh/?utm_source=dlt&utm_medium=readme&utm_campaign=sponsorship) is a drop-in replacement for GitHub-hosted runners that speed up our CI/CD pipelines by 2x and up to 75% cheaper. We're grateful to Blacksmith for sponsoring us with free CI/CD minutes--which helps us keep builds fast and our costs lower. ## License `dlt` is released under the [Apache 2.0 License](LICENSE.txt). --- ## File: deploy/dlt/README.md Example `Dockerfile` that installs `dlt` package on an alpine linux image. For actual pipeline deployment please refer to [deploy a pipeline walkthrough](https://dlthub.com/docs/walkthroughs/deploy-a-pipeline/deploy-with-github-actions) --- ## File: dlt/destinations/impl/bigquery/README.md # Loader account setup 1. Create a new services account, add private key to it and download the `services.json` file. 2. Make sure the newly created account has access to BigQuery API. 3. You must add the following roles to the account above: `BigQuery Data Editor`, `BigQuey Job User` and `BigQuery Read Session User` (storage API) 4. IAM to add roles is here https://console.cloud.google.com/iam-admin/iam?project=chat-analytics-rasa-ci --- ## File: dlt/destinations/impl/mssql/README.md # loader account setup 1. Create new database `CREATE DATABASE dlt_data` 2. Create new user, set password `CREATE USER loader WITH PASSWORD = 'loader';` 3. Set as database owner (we could set lower permission) `ALTER DATABASE dlt_data OWNER TO loader` --- ## File: dlt/destinations/impl/postgres/README.md # loader account setup 1. Create new database `CREATE DATABASE dlt_data` 2. Create new user, set password `CREATE USER loader WITH PASSWORD 'loader';` 3. Set as database owner (we could set lower permission) `ALTER DATABASE dlt_data OWNER TO loader` --- ## File: dlt/destinations/impl/redshift/README.md # Public Access setup There's *Modify publicly accessible settings* in Actions of each Redshift cluster. Assign your IP there. # Runtime optimization https://www.intermix.io/blog/top-14-performance-tuning-techniques-for-amazon-redshift/ 1. we should use separate work queue for loader user 2. they suggest to not use dist keys 3. data must be inserted in order of sortkey # loader account setup 1. Create new database `CREATE DATABASE dlt_ci` 2. Create new user, set password 3. Set as database owner (we could set lower permission) `ALTER DATABASE dlt_ci OWNER TO loader` # Public access setup for Serverless Follow https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-connecting.html `Connecting from the public subnet to the Amazon Redshift Serverless endpoint using Network Load Balancer` that will use terraform template to create load balancer endpoint and assign public IP. The cost of the load balancer is ~16$/month + cost of IP It seems that port 5439 is closed to the VPC on which serverless redshift created itself. In the cluster panel: Data Access : VPC security group add Inbound Rule to allow 5439 port from any subnet 0.0.0.0/0 --- ## File: dlt/destinations/impl/weaviate/README.md ## Running Weaviate locally Start Weaviate with Docker Compose: ```sh docker compose -f tests/load/weaviate/docker-compose.yml up -d ``` Stop and clean up: ```sh docker compose -f tests/load/weaviate/docker-compose.yml down -v --remove-orphans ``` This starts Weaviate with the contextionary vectorizer (no external APIs required). Add to `config.toml`: ```toml [destination.weaviate] connection_type = "local" vectorizer = "text2vec-contextionary" module_config = {text2vec-contextionary = {vectorizeClassName = false, vectorizePropertyName = true}} ``` For more details, see [Weaviate Local Quickstart](https://weaviate.io/developers/weaviate/quickstart/local). --- ## File: dlt/helpers/marimo/README.md # How to add a marimo widget This guide shows how to add the hypothetical `pipeline_browser` widget. A marimo widget **is** a marimo notebook. We'll call this the *widget notebook*. ## 1. Create a new marimo notebook under `dlt/helpers` Create the notebook with ```shell marimo edit dlt/helpers/_pipeline_browser.py ``` The widget notebook file name is prefixed with `_` for convention and to avoid name collisions. ## 2. Edit your widget notebook Edit the `_pipeline_browser.py` notebook via the marimo GUI until you're satisfied. Tips: - Use a [setup cell](https://docs.marimo.io/guides/reusing_functions/#1-create-a-setup-cell) for your import and constants. - Set a cell names to be able to [run cells directly](https://docs.marimo.io/api/cell/) in a unit test. This is done by setting a function name instead of the anonymous `_` - If you want your widget to take input arguments, define these variables inside the setup cell and set them to `None`. In downstream cells, you can use `mo.stop(VAR is None)` to hide cells if the argument is missing The next snippet shows the setup cell of a widget that takes the argument `pipeline_name`. The cell that depends on it to instantiate a pipeline via `dlt.attach(pipeline_name)` starts with `mo.stop(pipeline_name is None)` ```python # dlt/helpers/marimo/_pipeline_browser.py with app.setup: from typing import Any, cast from itertools import chain import marimo as mo import dlt from dlt.common.utils import without_none pipeline_name = None @app.cell def _(): mo.stop(pipeline_name is None) pipeline = dlt.attach(pipeline_name) return (pipeline,) # ... ``` ## 3. Register the widget To make the widget publicly available via `dlt`, you need to modify `dlt/helpers/marimo/__init__.py`. The following is required: 1. import the `app` variable from the widget notebook `.py` file and give it an alias 2. add this alias to the `__all__` clause 3. create a function that instantiates the widget from the `app` argument and has the input parameters 4. add an `if/else` condition inside `render()` to render the widget The following snippet shows what's required to render the `_pipeline_browser.py` widget ```python import marimo import mowidgets from dlt.helpers.marimo._pipeline_browser import app as pipeline_browser # pre-existing function def render(app: marimo.App, *args, **kwargs): if not isinstance(app, marimo.App): raise ValueError("app must be an instance of marimo.App") if ...: ... # add new condition elif app is pipeline_browser: return pipeline_browser_widget(app, *args, **kwargs) else: raise ValueError("app must be either load_package_viewer or schema_viewer") # ... def pipeline_browser_widget(app, pipeline_name, *args, **kwargs): return mowidgets.widgetize( app, data_access=True, # this must match the input name in the notebook inputs={"pipeline_name": pipeline_name} ) __all__ = ( "render", ..., # add the aliased `app` variable; not the newly created `_widget` function "pipeline_browser", ) ``` # 4. Trying the widget Open a new marimo notebook with `marimo edit dev.py`. To use the widget, import `render` and the app object from the `dlt.helpers.marimo`. Calling `render(app)` will return a widget object. Calling `await` on it will render it. Example snippet ```python # dev.py @app.cell def _(): import marimo as mo from dlt.helpers.marimo import render, pipeline_browser return pipeline_browser, render @app.cell async def _(pipeline_browser, render): # to display directly await render(pipeline_browser) return # call `await` on `render()` to display the widget directly @app.cell async def _(pipeline_browser, render): await render(pipeline_browser) return # or assign a variable to display elsewhere and access the widget's data @app.cell async def _(pipeline_browser, render): w = render(pipeline_browser) await w return (w,) @app.cell def _(w): w.data return ``` Alternatively, you can skip the render function and call the `_widget()` function on the app object directly ```python @app.cell def _(): import marimo as mo from dlt.helpers.marimo import pipeline_browser_widget, pipeline_browser return pipeline_browser, pipeline_browser_widget @app.cell async def _(pipeline_browser, pipeline_browser_widget): # to display directly await pipeline_browser_widget(pipeline_browser) return ``` ## 5. unit tests Add tests for cells with complex operations. You can retrieve a cell by name and give it inputs. Source code for the widget ```python # dlt/helpers/marimo/_pipeline_browser.py @app.cell def selector(base_path): pipelines = [p.name for p in pathlib.Path(base_path).expanduser().iterdir()] select_pipeline = mo.ui.dropdown(pipelines, value="pokemon") select_pipeline return (select_pipeline,) ``` Unit test ```python # tests/helpers/marimo/test_pipeline_browser.py # import the named cell directly from the widget notebook from dlt.helpers.marimo._pipeline_browser import selector def test_cell_selector(): base_path = ... # outputs is the HTML to be displayed # definitions is a dictionary of values returned outputs, definitions = selector.run(base_path=base_path) assert definitions["select_pipeline"] == ... ``` ## 6. Integration tests You can run the full widget notebook to see if it can run "end-to-end" for a set of definitions. These input definitions can override nodes of the DAG, which allows to test various states of the application. ```python # tests/helpers/marimo/test_pipeline_browser.py import pytest # import the aliased app from dlt.helpers.marimo import pipeline_browser @pytest.mark.parametrize("input1", [...]) @pytest.mark.parametrize("input2", [...]) def test_pipeline_browser(input1, input2) -> None: input_definitions = { "input1": input1, "input2": input2, } outputs, definitions = pipeline_browser.run(defs=input_definitions) assert ... ``` --- ## File: docs/examples/archive/README.md # Writing Source Extractors `dlt` sources are iterators or lists and writing them does not require any additional knowledge beyond basic python. `dlt` sources are also pythonic in nature: they are simple, can be chained, pipelined and composed like any other python iterator or a sequence. # Examples 1. `quickstart` loads a nested json document into `duckdb` and then queries it with built in `sql_client` demonstrating the parent-child table joins. 1. `sql_query` source and `read_table` example. This source iterates over any `SELECT` statement made against database system supported by `SqlAlchemy`. The example connects to Redshift and iterates a table containing Ethereum transactions. Shows the inferred schema (which nicely preserves typing). Mind that our source is a one-liner :) 1. `rasa` example and `rasa_tracker_store` source extracts rasa tracker store events to a set of inferred tables. It shows a few common patterns - shows how to **pipeline resources**: it depends on a "head" resource that reads base data (ie. events from kafka/postgres/file). the dependent resource is called `transformer` - it shows how to write **stream resource** which creates table schemas and sends data to those tables depending on the event type - it stores `last_timestamp_value` in the state 1. `singer_tap`, `stdout` and `singer_tap_example` is fully functional wrapper for any singer/meltano source - clones the desired tap, installs it and runs it in a virtual env - passes the catalog and config files - like rasa it is a **transformer** (on stdio pipe) and `stream` resource - it stores singer state in `dlt` state 1. `singer_tap_jsonl_example` like the above but instead of process pipe it reads singer messages from file. it creates a huge hubspot schema. 1. `google_sheets` a source that returns values from specified sheet. The example takes a sheet, infers a schema, loads it to BigQuery/Redshift and displays inferred schema. it uses the `secrets.toml` to manage credentials and is an example of one-liner pipeline 1. `chess` an example of a pipeline project with its own config and credential files. it is also an example of how transformers are connected to resources and resource selection. **it should be run from examples/chess` folder**. It also shows: **how to use retry decorator** and **how to run resources/transformers in parallel with a decorator** 2. `chess/chess_dbt.py`: an example of a `dbt` transformations package working with a dataset loaded by `dlt`. The package is incrementally processing the loaded data following the new loaded packages stored in `_dlt_loads` table at the end of every pipeline run. Note the automatic usage of isolated virtual environment to run dbt and sharing of the credentials. 1. `run_dbt_jaffle` runs dbt's jaffle shop example taken directly from the github repo and queries the results with `sql_client`. `duckdb` database is used to load and transform the data. The database `write` access is passed from `dlt` to `dbt` and back. Not yet ported: 1. `discord_iterator` an example that load example discord data (messages, channels) into warehouse from supplied files. Shows several auxiliary pipeline functions and an example of pipelining iterators (with `map` function). You can also see that produced schema is quite complicated due to several layers of nesting. 2. `ethereum` source shows that you can build highly scalable, parallel and robust sources as simple iterators. --- ## File: docs/examples/CONTRIBUTING.md # How to contribute your example Note: All paths in this guide are relative to the `dlt` repository directory. ## Add snippet - Go to `docs/examples/`. - Copy the template in `./_template/..`. - Make sure the folder and your examples script have the same name - Update the doc string which will compromise the generated markdown file, check the other examples how it is done - If your example requires any secrets, add the vars to the example.secrects.toml but do not enter the values. - Add your example code, make sure you have a `if __name__ = "__main__"` clause in which you run the example script, this will be used for testing - You should add one or two assertions after running your example ## Testing - You can test your example simply by running your example script from your example folder. On CI a test will be automatically generated. ## Checking your generated markdown The command `npm start` starts a local development server and opens up a browser window. - To install npm read [README](../website/README.md). - You should your example be automatically added to the examples section in the local version of the docs. Check the rendered output and see wether it looks the way you intended. ## Add ENV variables If you use any secrets for the code snippets, e.g. Zendesk requires credentials. Please talk to us. We will add them to our google secrets vault. ## Add dependencies If your example requires any additional dependency, then you can add it - To `pyproject.toml` in the `[dependency-groups]` section in the `docs` group. - Do not forget to update your `uv.lock` file with `uv sync` command and commit.