Python
The skippr package runs the Skipprd engine inside Python. You build a pipeline with Config, run it with Session, and read the results as Arrow tables. It is the same engine as the skipprd command, and both read and write the same skippr.yml, so you can build a config in a notebook and run it from cron with the CLI.
Before you begin
- Python 3.10 or later on macOS arm64 or Linux x86_64.
- Install the package, and the pandas extra if you want
.to_pandas():
pip install skippr
pip install "skippr[pandas]" # optional
python -c "import skippr; print(skippr.Session)"The package ships type stubs, so your editor, mypy, and coding assistants see every connector class, field, and docstring. Connectors download on first use from install.skippr.io. See Install for details.
Run a pipeline
Session binds the engine to one pipeline. Each Python call has a CLI equivalent.
import skippr
cfg = skippr.Config.discover()
s = skippr.Session(cfg.get_pipeline("bikehire"))
s.doctor()
s.discover()
s.sync(once=True)
s.df()skipprd doctor
skipprd discover --pipeline bikehire
skipprd sync --pipeline bikehire --once
skipprd df --pipeline bikehireConfig.discover()loadsskippr.ymlfrom the current directory, or starts an empty config that saves there.Config.load(path)loads a specific file.Sessiontakes aPipelineReffrom aConfig, never a name string. To run another pipeline, open anotherSession.discover()samples the source and records the schema.sync(once=True)runs one pass and returns.sync()keeps running, one pass everysync_frequency_seconds(900 by default).
When you create a Session, it takes a snapshot of its pipeline's config and resolves every ${NAME} reference from the environment, including a .env file next to skippr.yml. A missing variable raises ValueError there, before any data moves, not halfway through a sync.
Build a config
Every connector is a typed class named for its role: DataSourceS3, DataSinkPostgres, SchemaSinkGlue. Required fields are required keyword arguments. Registering a connector returns a typed ref, and Pipeline takes refs, not "data_sources.x" strings, so a typo is an error in your editor.
import skippr
from skippr import Config, DataSourceS3, LocalStorage, Pipeline
cfg = Config().workspace("bikehire").storage(LocalStorage())
src = cfg.data_source(
"sample",
DataSourceS3(s3_bucket="skippr-public-sample-data", s3_prefix="bike-hire"),
)
bikehire = cfg.pipeline("bikehire", Pipeline(data_source=src, sync_frequency_seconds=60))
cfg.save("skippr.yml")
s = skippr.Session(bikehire)
s.discover()skipprd --workspace bikehire --storage-mode local connect data-source s3 \
--pipeline bikehire \
--name sample \
--s3-bucket skippr-public-sample-data \
--s3-prefix bike-hire
skipprd discover --pipeline bikehireskippr:
workspace: bikehire
skipprd_el_storage_mode: local
pipelines:
bikehire:
data_source: data_sources.sample
sync_frequency_seconds: 60
data_sources:
sample:
S3:
s3_bucket: skippr-public-sample-data
s3_prefix: bike-hireWhere Skipprd keeps its own state is a class too:
LocalStorage()keeps state in the local data directory (./databy default). Good for trying things out and single-machine runs.S3Storage("your-state-bucket")keeps state in an S3 bucket instead of on local disk.
cfg.store(...) chooses where read progress is tracked: SledStore() (the local on-disk state store, the default) or DynamoDbStore("table"). See State store.
cfg.to_yaml() shows the config as YAML without writing it. cfg.save() writes back to the file the config was loaded from; cfg.save(path) writes somewhere else. save writes YAML only and refuses .json and .toml paths.
Update an existing config
Registering a name again, save, and skipprd connect all merge the same way, so Python and the CLI never fight over a file:
- Each top-level field you set replaces that field. A nested field such as
object_storeis replaced whole. - Fields you did not set, other pipelines, and other entries are kept.
- An entry cannot change connector type. Re-registering
sampleasDataSourceFilewhen the file hasS3is an error. LocalStorage()removes anyskippr_s3_bucket.
from skippr import Config, DataSourceS3
disk = Config.discover()
disk.data_source("sample", DataSourceS3(s3_bucket="skippr-public-sample-data", s3_prefix="bike-hire-2026"))
disk.save()skipprd connect data-source s3 \
--pipeline bikehire \
--name sample \
--s3-bucket skippr-public-sample-data \
--s3-prefix bike-hire-2026Registration and save check the whole merged config, so you find problems when you write the file rather than when a pipeline runs:
- every entry names a known connector, and unknown keys outside connector blocks are an error;
- every secret is a
${NAME}reference — a plaintext secret already in the file blockssaveuntil you replace it; - every pipeline with a source is valid;
- no two SkipprLake or DuckDB destinations write to the same table namespace;
- every destination matches the schema sink it links to.
Keep secrets out of the file
Secret fields accept only EnvRef, never a plain string, so a password cannot reach skippr.yml. Export the variable before you create the Session:
export POSTGRES_PASSWORD="your-password"from skippr import Config, DataSinkPostgres, EnvRef, Pipeline
cfg = Config.discover()
src = cfg.get_data_source("sample")
warehouse = cfg.data_sink(
"warehouse",
DataSinkPostgres(
host="localhost",
user="skippr",
password=EnvRef("POSTGRES_PASSWORD"),
database="analytics",
),
)
cfg.pipeline("bikehire", Pipeline(data_source=src, data_sink=warehouse))
cfg.save()skipprd connect data-sink postgres \
--pipeline bikehire \
--name warehouse \
--host localhost \
--user skippr \
--password '${POSTGRES_PASSWORD}' \
--database analyticsdata_sinks:
warehouse:
Postgres:
host: localhost
user: skippr
password: ${POSTGRES_PASSWORD}
database: analyticsAll three write password: ${POSTGRES_PASSWORD}. Path-like fields typed as str, such as Snowflake private_key_path, take the "${NAME}" string directly.
Nested and tagged fields
Nested settings are their own frozen classes, named after their connector. A field that accepts one of several shapes takes one class per variant. For example, a SkipprLake destination whose files live in Cloudflare R2:
from skippr import Config, DataSinkSkipprLake, DataSinkSkipprLakeWarehouseObjectStoreR2, EnvRef
cfg = Config.discover()
lake = cfg.data_sink(
"lake",
DataSinkSkipprLake(
warehouse="s3://my-iceberg-warehouse/",
catalog_table="my-iceberg-catalog",
object_store=DataSinkSkipprLakeWarehouseObjectStoreR2(
endpoint="https://<account>.r2.cloudflarestorage.com",
access_key_id="<access-key-id>",
secret_access_key=EnvRef("OBJECTS_SECRET_ACCESS_KEY"),
),
),
schema_sink="lake",
)Fields with a fixed set of values are Literal types. DataSourceGoogleSerpRanks(keywords=[], targets=[], device="tablet") is a type error in your editor and a ValueError at runtime, because device accepts only "desktop" or "mobile".
Schema sinks
A schema sink is where Skipprd writes table definitions. How you attach one depends on the destination.
SkipprLake, AthenaIceberg, and Duckdb use one config for both roles. Pass schema_sink= a name, and the same config is registered under schema_sinks.<name>, as in the example above.
Other destinations link a schema sink you registered first. Athena with Glue:
from skippr import Config, DataSinkAthena, Pipeline, SchemaSinkGlue
cfg = Config.discover()
src = cfg.get_data_source("sample")
glue = cfg.schema_sink(
"glue",
SchemaSinkGlue(
s3_bucket="your-output-bucket",
s3_prefix="bikehire",
athena_workgroup_name="primary",
athena_results_s3_bucket="your-output-bucket",
glue_database_name="bikehire",
),
)
athena = cfg.data_sink(
"athena",
DataSinkAthena(
s3_bucket="your-output-bucket",
s3_prefix="bikehire",
athena_workgroup_name="primary",
athena_results_s3_bucket="your-output-bucket",
),
schema_sink=glue,
)
cfg.pipeline("bikehire", Pipeline(data_source=src, data_sink=athena))
cfg.save()Refs belong to the Config that returned them. Passing a ref from one Config to another is an error.
Read data with df and query
df() and query() read the data Skipprd holds for this pipeline — the same views skipprd query shows. They read the WAL, combined with the destination's Iceberg tables when the destination is SkipprLake, AthenaIceberg, or DuckDB. For any other destination they read the WAL only.
import skippr
s = skippr.Session(skippr.Config.discover().get_pipeline("bikehire"))
s.df() # every namespace in this pipeline
s.df("rides") # one namespace: bikehire.rides
s.query("SELECT count(*) FROM bikehire.rides") # any SQL over this pipeline's views
s.df().schema # the Arrow schema
s.df("rides").to_pandas() # needs skippr[pandas]skipprd df --pipeline bikehire
skipprd df --pipeline bikehire --namespace rides
skipprd query --sql "SELECT count(*) FROM bikehire.rides"A namespace is one table's worth of records. By default a pipeline has one namespace, named after the pipeline; transforms can split records into several. The examples assume a transform has split out a rides namespace. Both methods return a pyarrow.Table.
Check a config before a long run
doctor() (or skipprd doctor) checks that the config is complete and consistent: at least one pipeline and source, every reference points at an entry that exists, the storage mode, and the whole-config rules above. It returns a dict with ok and a list of checks. It does not connect to your source or destination; discover() is the first call that does.
import skippr
s = skippr.Session(skippr.Config.discover().get_pipeline("bikehire"))
result = s.doctor()
if not result["ok"]:
for check in result["checks"]:
print(check)Run without a destination
Leave out data_sink and the pipeline still syncs. The WAL is the dataset, and df() and query() read it. When you want a warehouse, register the destination, register the pipeline again with data_sink=, and open a new Session. Skipprd does not compact or reclaim the WAL until the pipeline has a destination, so plan disk space for long WAL-only runs.
dbt and Soda
Session loads raw (bronze) data. It does not run dbt.
s.sync(once=True)lands bronze data in the WAL, and in the destination if you set one.dbt runbuilds your silver and gold models in the warehouse.dbt testandsoda scancheck the results in that warehouse.
Next steps
- Quickstart: PostgreSQL or Snowflake — land data in a warehouse from Python.
skippr.ymlreference — every engine and pipeline setting.- Data sources — the connector classes and their options.
- Transforms — namespaces, partitions, and flattening.
- How Skipprd works — what happens between source and destination.
