Skip to main content

Contract

A declarative boundary contract for pipeline data.

Usage

Source

Contract(
    name,
    direction="source",
    schema=None,
    steps=list(),
    version=None,
    owner=None,
    consumers=None,
    description=None,
    thresholds=None,
    on_violation="warn",
)

A Contract is a written-down agreement about what a table must look like at a specific point in a data pipeline. It bundles three things into one object: a Schema with the expected column names and types (the structural expectations), a list of Step objects with the rules the values must follow (the semantic expectations), and metadata saying who owns the data, who depends on it, and which version of the agreement is in force.

With a plain Validate chain, the rules exist only as code that runs against one table. A contract holds the same rules as data. It isn’t tied to any table, so you can apply it to each day’s delivery of a feed, check it into version control, review changes to it like any other change, and share it as a YAML file. Teams on both sides of a handoff can then refer to the same definition of “valid”. The producer of a table can check its output against the contract before publishing, and the consumer can check its input against the same contract on arrival.

Use a contract when the expectations matter more than any one validation run: data crossing a team or system boundary, inputs and outputs of a scheduled job, or a dataset with named owners and downstream consumers. For a one-off look at a table, a Validate chain is simpler.

Validating data against a contract gives you an ordinary interrogated Validate object. That means everything you already use with validations works here: the tabular report, step reports, all_passed(), assert_passing(), data extracts, and so on.

Parameters

name: str

A short, unique name for the contract (e.g., "raw_clickstream_feed"). It becomes the table name in the validation report and is part of the report label. It must be a non-empty string.

direction: Literal["source", "target"] = "source"

Where the contract sits relative to a transformation: "source" (the default) for data flowing in, or "target" for the data product flowing out. The direction is descriptive metadata that appears in violation messages from Pipeline. It doesn’t change what gets checked.

schema: Schema | None = None

An optional Schema giving the expected column names and data types. When a schema is given, it’s checked first, with col_schema_match() and its default settings. These defaults are strict: the table must have exactly the schema’s columns, in the same order, with matching types.

steps: list[Step] = list()

A list of Step objects, each naming a validation method and its arguments (e.g., pb.Step("col_vals_gt", columns="amount", value=0)). They run in list order after the schema check. Only a subset of validation methods can be used. See the Supported Step Methods section below.

version: str | None = None

A version string for the contract, such as "1.2.0". Semantic versioning is a good convention, but any string is accepted. The version is added to the report label (e.g., "Contract: orders v1.2.0") and recorded in the report’s governance metadata.

owner: str | None = None

The person or team responsible for the contract and the data it describes. It’s recorded in the report’s governance metadata.

consumers: str | list[str] | None = None

Who depends on data meeting this contract, as a string or a list of strings. It’s recorded in the report’s governance metadata.

description: str | None = None

An optional longer description of the contract’s purpose. It’s kept with the contract and serialized with it, but doesn’t appear in validation reports.

thresholds: Thresholds | None = None

Default Thresholds for the validation built from this contract. These set when a failing step is considered a ‘warning’, ‘error’, or ‘critical’ issue. If the contract is used in a Pipeline that has its own thresholds, the pipeline’s thresholds take precedence.

on_violation: Literal["warn", "raise", "log"] = "warn"
What a Pipeline should do when data fails this contract: "warn" (the default) issues a UserWarning, "raise" raises a ContractViolationError (a RuntimeError), and "log" writes a warning to the "pointblank.contract" logger. This setting applies only when the contract is enforced through a Pipeline. Calling validate() directly always returns the validation without warning or raising, so check the result yourself (e.g., with assert_passing()).

Returns

Contract
A Contract object. Apply it to data with validate(), or with to_validate() when you want to add more steps before interrogating.

How A Contract Becomes A Validation

A contract holds no data and runs nothing until it’s applied to a table. Calling to_validate() (or validate(), which also interrogates) compiles the contract into a Validate object:

  1. a Validate is created for the table, with tbl_name= set to the contract name, a label of the form "Contract: <name> v<version>", and the contract’s thresholds=, owner=, consumers=, and version=
  2. if the contract has a schema, a col_schema_match() step is added first
  3. each Step is added in order, by calling the validation method it names with its stored arguments

A step that lists several columns expands into one validation step per column, just as it would in a Validate chain. Mistakes in a step’s arguments (a misspelled parameter name, for example) surface when the contract is compiled, not when it’s created.

Supported Step Methods

These validation methods can be used in a contract’s steps:

  • column value checks: col_vals_gt, col_vals_lt, col_vals_ge, col_vals_le, col_vals_eq, col_vals_ne, col_vals_between, col_vals_outside, col_vals_in_set, col_vals_not_in_set, col_vals_increasing, col_vals_decreasing, col_vals_null, col_vals_not_null, col_vals_regex, col_vals_within_spec, and col_vals_expr
  • column-level checks: col_exists and col_pct_null
  • table-level checks: col_schema_match, col_count_match, row_count_match, rows_distinct, and rows_complete
  • multi-condition checks: conjointly, with its expressions given as a list in exprs=

Each takes the same arguments as the Validate method of the same name. A step that names any other method is rejected with a ValueError when the contract is created. For steps that a contract can’t express, compile the contract with to_validate() and chain the extra methods onto the result.

Serialization

Contracts are meant to live outside your code, so they round-trip through YAML and plain dictionaries. to_yaml() and from_yaml() work with YAML files, and to_dict() and from_dict() work with dictionaries (useful for JSON or a configuration system). The schema is stored as a mapping of column names to type strings, the thresholds as a mapping of levels to values, and each step as a single-key mapping from method name to arguments. Step arguments must be plain data (strings, numbers, booleans, dates, lists, and dictionaries) for a contract to be written to YAML, although step-level Thresholds objects and tuples are converted to dictionaries automatically. A contract with other objects in its steps, such as a column selector (e.g., pb.starts_with("amt_")), a Polars expression, or a function, can’t be written: to_yaml() raises a ValueError that says where the object is. Keep contracts like that in Python.

Working With Other Pointblank Tools

A contract is the building block for several other parts of the API:

  • Pipeline pairs a source contract and a target contract around a transformation function. It validates the input, runs the transformation, validates the output, and applies each contract’s on_violation= setting. The results come back in a PipelineResult.
  • export_contract() converts a contract into an external format (JSON Schema, Frictionless Table Schema, dbt, or the Open Data Contract Standard), and import_contract() reads one of those formats. Call to_contract() on the result to get a Contract. Each format supports a different set of rules, so checks with no equivalent in the target format are left out of the export.
  • Because to_validate() returns an ordinary Validate object, a contract can be combined with reusable Steps libraries through Validate.add_steps().

Examples

Let’s write a contract for a daily orders extract. The schema fixes the four columns and their types, and the steps say that IDs must be present, each order must appear once, amounts must be positive, and the status must be one of three known values. Ownership and versioning go in the metadata, and the thresholds flag any failing test unit as a warning and a failure rate of 10% or more as an error.

import pointblank as pb
import polars as pl

orders_contract = pb.Contract(
    name="orders",
    version="1.2.0",
    owner="sales-data-team",
    consumers=["finance", "analytics"],
    description="Daily order extract delivered by the sales platform.",
    schema=pb.Schema(
        order_id="String",
        customer_id="Int64",
        amount="Float64",
        status="String",
    ),
    steps=[
        pb.Step("col_vals_not_null", columns=["order_id", "customer_id"]),
        pb.Step("rows_distinct", columns_subset=["order_id"]),
        pb.Step("col_vals_gt", columns="amount", value=0),
        pb.Step("col_vals_in_set", columns="status", set=["pending", "shipped", "delivered"]),
    ],
    thresholds=pb.Thresholds(warning=1, error=0.10),
)

orders_contract
Contract(name='orders', direction='source', version='1.2.0', schema=<defined>, steps=4)

The contract itself is just a definition. Printing it shows a short summary: the name, direction, version, whether a schema is set, and the number of steps.

Now let’s apply the contract to a day’s extract that meets every expectation. The validate() method compiles the contract against the table and interrogates it, returning a Validate object that displays as a validation report.

orders_day_1 = pl.DataFrame(
    {
        "order_id": ["A1", "A2", "A3", "A4"],
        "customer_id": [101, 102, 101, 103],
        "amount": [25.00, 110.50, 42.00, 8.99],
        "status": ["pending", "shipped", "delivered", "shipped"],
    }
)

orders_contract.validate(orders_day_1)
Pointblank Validation
Contract: orders v1.2.0
PolarsordersWARNING1ERROR0.1CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
○ ○ — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
order_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 3
col_vals_not_null
col_vals_not_null()
customer_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 4
rows_distinct
rows_distinct()
order_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 5
col_vals_gt
col_vals_gt()
amount 0 ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 6
col_vals_in_set
col_vals_in_set()
status pending, shipped, delivered ✓ 4 4
1.00
0
0.00
○ ○ — —
Owner: sales-data-teamConsumers: finance, analyticsVersion: 1.2.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 order_id String 1 order_id ✓ String ✓
2 customer_id Int64 2 customer_id ✓ Int64 ✓
3 amount Float64 3 amount ✓ Float64 ✓
4 status String 4 status ✓ String ✓
Supplied Column Schema:
[('order_id', 'String'), ('customer_id', 'Int64'), ('amount', 'Float64'), ('status', 'String')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The report header shows the label built from the contract (Contract: orders v1.2.0) along with the owner, consumers, and version. The first step is the schema check, followed by one step per contract rule. The col_vals_not_null step names two columns, so it becomes two validation steps. All six steps pass.

The next day’s extract has problems: a missing customer ID, a repeated order ID, a negative amount, and a status the contract doesn’t allow. The same contract catches all of them.

orders_day_2 = pl.DataFrame(
    {
        "order_id": ["A5", "A6", "A6", "A8"],
        "customer_id": [104, None, 101, 105],
        "amount": [25.00, -5.00, 42.00, 8.99],
        "status": ["pending", "shipped", "returned", "shipped"],
    }
)

validation = orders_contract.validate(orders_day_2)

validation
Pointblank Validation
Contract: orders v1.2.0
PolarsordersWARNING1ERROR0.1CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
○ ○ — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
order_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#EBBC14 3
col_vals_not_null
col_vals_not_null()
customer_id — ✓ 4 3
0.75
1
0.25
● ● —
#EBBC14 4
rows_distinct
rows_distinct()
order_id — ✓ 4 2
0.50
2
0.50
● ● —
#EBBC14 5
col_vals_gt
col_vals_gt()
amount 0 ✓ 4 3
0.75
1
0.25
● ● —
#EBBC14 6
col_vals_in_set
col_vals_in_set()
status pending, shipped, delivered ✓ 4 3
0.75
1
0.25
● ● —
Owner: sales-data-teamConsumers: finance, analyticsVersion: 1.2.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 order_id String 1 order_id ✓ String ✓
2 customer_id Int64 2 customer_id ✓ Int64 ✓
3 amount Float64 3 amount ✓ Float64 ✓
4 status String 4 status ✓ String ✓
Supplied Column Schema:
[('order_id', 'String'), ('customer_id', 'Int64'), ('amount', 'Float64'), ('status', 'String')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The schema still matches, but four of the rule steps have failing test units. With only four rows, one failure is already 25% of the table, so those steps exceed both the warning and the error thresholds. The rows_distinct step counts two failures because both rows with order "A6" are duplicates. Since the result is an ordinary Validate object, you can check it in code, for example to stop a scheduled job:

validation.all_passed()
False

To share the contract with another team or keep it under version control, write it out as YAML. Called without a path, to_yaml() returns the YAML text.

print(orders_contract.to_yaml())
contract:
  name: orders
  version: 1.2.0
  owner: sales-data-team
  consumers:
  - finance
  - analytics
  description: Daily order extract delivered by the sales platform.
  schema:
    order_id: String
    customer_id: Int64
    amount: Float64
    status: String
  steps:
  - col_vals_not_null:
      columns:
      - order_id
      - customer_id
  - rows_distinct:
      columns_subset:
      - order_id
  - col_vals_gt:
      columns: amount
      value: 0
  - col_vals_in_set:
      columns: status
      set:
      - pending
      - shipped
      - delivered
  thresholds:
    warning: 1
    error: 0.1

The YAML lists the metadata, schema, steps, and thresholds in a form that’s easy to read and review. from_yaml() reads it back into an equivalent Contract.

Finally, a contract can be the starting point for a stricter, local validation. Here, to_validate() compiles the contract without interrogating, and we add one more check (orders of 100 or less) before calling interrogate().

(
    orders_contract.to_validate(orders_day_1)
    .col_vals_le(columns="amount", value=100)
    .interrogate()
)
Pointblank Validation
Contract: orders v1.2.0
PolarsordersWARNING1ERROR0.1CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
○ ○ — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
order_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 3
col_vals_not_null
col_vals_not_null()
customer_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 4
rows_distinct
rows_distinct()
order_id — ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 5
col_vals_gt
col_vals_gt()
amount 0 ✓ 4 4
1.00
0
0.00
○ ○ — —
#4CA64C 6
col_vals_in_set
col_vals_in_set()
status pending, shipped, delivered ✓ 4 4
1.00
0
0.00
○ ○ — —
#EBBC14 7
col_vals_le
col_vals_le()
amount 100 ✓ 4 3
0.75
1
0.25
● ● —
Owner: sales-data-teamConsumers: finance, analyticsVersion: 1.2.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 order_id String 1 order_id ✓ String ✓
2 customer_id Int64 2 customer_id ✓ Int64 ✓
3 amount Float64 3 amount ✓ Float64 ✓
4 status String 4 status ✓ String ✓
Supplied Column Schema:
[('order_id', 'String'), ('customer_id', 'Int64'), ('amount', 'Float64'), ('status', 'String')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The six contract steps pass as before, and the added seventh step flags the order of 110.50. The contract itself is unchanged, so other users of it aren’t affected by the extra rule.

See Also

Step

A single declarative validation step, used in a contract’s steps=.

Schema

The column names and types used in a contract’s schema=.

Pipeline

Enforce source and target contracts around a transformation.

export_contract()

Convert a contract to JSON Schema, Frictionless, dbt, or ODCS.

import_contract()

Read an external format, then call to_contract() on it.

Methods

Name Description
validate() Check a table against the contract.
to_validate() Compile the contract into a validation plan for a table, without running it.
to_yaml() Write the contract as YAML.
from_yaml() Read a contract from a YAML file.
to_dict() Convert the contract to a dictionary.
from_dict() Create a contract from a dictionary.

validate()

Check a table against the contract.

Usage

Source

validate(data)

This is the usual way to apply a contract. It compiles the contract into a validation plan for the table and interrogates it right away. This is the same as calling to_validate() and then interrogate(). The result is an ordinary interrogated Validate object. You can display it as a report, inspect individual steps with get_step_report(), pull out failing rows with get_data_extracts(), or check the outcome in code.

A failing contract doesn’t raise an error or warning here, whatever the contract’s on_violation= setting is. That setting is applied by Pipeline. When you call validate() yourself, check the result with a method like all_passed(), assert_passing(), or assert_below_threshold(). To add checks before the validation runs, use to_validate() instead.

Parameters

data: IntoDataFrame
The table to validate against the contract. Any table type that Validate accepts can be used (e.g., a Polars or Pandas DataFrame, or an Ibis table).

Returns

Validate
An interrogated Validate object with the results of every contract check.

Raises

TypeError
If a step’s arguments don’t fit its validation method (for example, a misspelled parameter name). Step arguments are only checked when the contract is compiled.

Examples

Here’s a contract for a sensor readings feed. Each reading needs a sensor ID, the temperature must be within a plausible range, and the feed must have five rows. Any failing test unit counts as an error.

import pointblank as pb
import polars as pl

readings_contract = pb.Contract(
    name="sensor_readings",
    owner="iot-platform",
    steps=[
        pb.Step("col_vals_not_null", columns="sensor_id"),
        pb.Step("col_vals_between", columns="temp_c", left=-40, right=60),
        pb.Step("row_count_match", count=5),
    ],
    thresholds=pb.Thresholds(error=1),
)

readings = pl.DataFrame(
    {
        "sensor_id": ["s1", "s2", None, "s4", "s5"],
        "temp_c": [21.5, 19.0, 22.1, 85.0, 20.4],
    }
)

validation = readings_contract.validate(readings)

validation
Pointblank Validation
Contract: sensor_readings
Polarssensor_readingsWARNING—ERROR1CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#EBBC14 1
col_vals_not_null
col_vals_not_null()
sensor_id — ✓ 5 4
0.80
1
0.20
— ● —
#EBBC14 2
col_vals_between
col_vals_between()
temp_c [-40, 60] ✓ 5 4
0.80
1
0.20
— ● —
#4CA64C 3
row_count_match
row_count_match()
— 5 ✓ 1 1
1.00
0
0.00
— ○ — —
Owner: iot-platform

The row count is right, but the report shows two problems: one reading has no sensor ID, and one temperature (85.0) is out of range. Both steps reach the error threshold. Because validate() doesn’t raise, a script or scheduled job decides what happens next. For example, it could test whether any step reached the error level:

validation.above_threshold(level="error")
True

In a pipeline where bad data should stop processing, assert_below_threshold() raises an AssertionError in this situation instead.

to_validate()

Compile the contract into a validation plan for a table, without running it.

Usage

Source

to_validate(data)

This method turns the contract into a Validate object for the given table. The validation is set up with the contract’s name, label, thresholds, and governance metadata (owner, consumers, and version). Then the schema check (if the contract has a schema) and each of the contract’s steps are added in order. Nothing is checked yet: the plan runs when you call interrogate() on it.

Use this method instead of validate() when you want to do something with the plan before it runs. Most often that’s adding checks that are local to one use of the data, or ones a contract can’t express, such as col_vals_str_len() or specially(). You can also attach Actions and final actions this way, or combine the contract with a reusable Steps library via add_steps(). The contract itself isn’t changed, so these additions don’t affect anyone else using it.

Parameters

data: IntoDataFrame
The table to validate against the contract. Any table type that Validate accepts can be used (e.g., a Polars or Pandas DataFrame, or an Ibis table).

Returns

Validate
A Validate object holding the contract’s checks, not yet interrogated.

Raises

TypeError
If a step’s arguments don’t fit its validation method (for example, a misspelled parameter name). Step arguments are only checked when the contract is compiled.

Examples

Let’s define a small contract for a table of products: a two-column schema, plus rules that product IDs are present and prices are positive.

import pointblank as pb
import polars as pl

products_contract = pb.Contract(
    name="products",
    version="2.0.0",
    schema=pb.Schema(product_id="String", price="Float64"),
    steps=[
        pb.Step("col_vals_not_null", columns="product_id"),
        pb.Step("col_vals_gt", columns="price", value=0),
    ],
)

products = pl.DataFrame(
    {
        "product_id": ["P-100", "P-101", "P-102", "X-103"],
        "price": [19.99, 5.49, 120.00, 7.25],
    }
)

Our team also wants product IDs to start with "P-". That rule isn’t part of the shared contract, so we compile the contract with to_validate(), add a col_vals_regex() step, and then interrogate.

(
    products_contract.to_validate(products)
    .col_vals_regex(columns="product_id", pattern=r"^P-")
    .interrogate()
)
Pointblank Validation
Contract: products v2.0.0
Polarsproducts
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
— — — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
product_id — ✓ 4 4
1.00
0
0.00
— — — —
#4CA64C 3
col_vals_gt
col_vals_gt()
price 0 ✓ 4 4
1.00
0
0.00
— — — —
#4CA64C66 4
col_vals_regex
col_vals_regex()
product_id ^P- ✓ 4 3
0.75
1
0.25
— — —
Version: 2.0.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 product_id String 1 product_id ✓ String ✓
2 price Float64 2 price ✓ Float64 ✓
Supplied Column Schema:
[('product_id', 'String'), ('price', 'Float64')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The first three steps come from the contract (the schema check, then the two rules) and all pass. The fourth step is our addition, and it flags the ID "X-103".

to_yaml()

Write the contract as YAML.

Usage

Source

to_yaml(path=None)

YAML is the natural way to store and share a contract. It can sit in version control next to the pipeline code, so changes to the rules go through review, and other teams can read it without running Python. This method writes the contract under a top-level contract: key, with the fields in a fixed order (name and metadata first, then the schema, steps, and thresholds). from_yaml() reads the file back into an equivalent Contract.

Fields that aren’t set, and the default direction= and on_violation= values, are left out of the YAML (see to_dict()). Step arguments must be plain data (strings, numbers, booleans, dates, lists, and dictionaries) so that from_yaml() can read them back. Step-level thresholds are converted to dictionaries automatically, but column selectors, expressions, and functions can’t be written.

Parameters

path: str | Path | None = None
An optional path to write the YAML to. Any missing parent directories are created, and an existing file is overwritten. If None (the default), nothing is written.

Returns

str
The YAML text. It’s returned whether or not path= is given.

Raises

ValueError
If a step argument can’t be represented in YAML (for example, a column selector such as pb.starts_with("amt_") or a Polars expression). The message names the step and argument. Nothing is written to path= in this case.

Examples

Let’s define a contract for a table of web sessions and get its YAML representation.

import pointblank as pb

sessions_contract = pb.Contract(
    name="web_sessions",
    version="3.1.0",
    owner="web-analytics",
    consumers=["marketing", "product"],
    on_violation="raise",
    schema=pb.Schema(session_id="String", duration_s="Int64", device="String"),
    steps=[
        pb.Step("col_vals_ge", columns="duration_s", value=0),
        pb.Step("col_vals_in_set", columns="device", set=["desktop", "mobile", "tablet"]),
    ],
)

print(sessions_contract.to_yaml())
contract:
  name: web_sessions
  version: 3.1.0
  owner: web-analytics
  consumers:
  - marketing
  - product
  on_violation: raise
  schema:
    session_id: String
    duration_s: Int64
    device: String
  steps:
  - col_vals_ge:
      columns: duration_s
      value: 0
  - col_vals_in_set:
      columns: device
      set:
      - desktop
      - mobile
      - tablet

The output lists the metadata first, including on_violation because it isn’t the default, then the schema and the steps. Each step is written as its method name with the arguments nested below it.

To save the contract, pass a file path. The parent directory is created if needed, and the YAML text is still returned. Reading the file back gives a contract equal to the original:

import tempfile
from pathlib import Path

contract_path = Path(tempfile.mkdtemp()) / "contracts" / "web_sessions.yaml"

_ = sessions_contract.to_yaml(contract_path)

pb.Contract.from_yaml(contract_path) == sessions_contract
True

The file holds everything needed to rebuild the contract. A natural place for it is version control, next to the code that produces the data, so that changes to the rules are reviewed like any other change. Wherever the contract is enforced, it can be loaded with from_yaml() (or as part of a pipeline with Pipeline.from_yaml()).

from_yaml()

Read a contract from a YAML file.

Usage

Source

from_yaml(path)

Keeping contracts in YAML files separates the definition of “valid data” from the code that checks it. The files are easy to read, can be reviewed and versioned like any other change, and can be shared between the teams that produce and consume a dataset. This method loads such a file into a Contract, ready to use with validate() or a Pipeline.

The file can hold the contract under a top-level contract: key, as to_yaml() writes it, or have the contract fields at the top level. The fields are the contract’s parameter names (see from_dict() for the expected structure). A hand-written contract file might look like this:

contract:
  name: payments
  version: 1.0.0
  owner: billing-team
  schema:
    payment_id: String
    amount: Float64
  steps:
  - col_vals_not_null:
      columns: payment_id
  - col_vals_gt:
      columns: amount
      value: 0
  thresholds:
    warning: 1

Parameters

path: str | Path
The path to the YAML file.

Returns

Contract
A new Contract object.

Raises

FileNotFoundError

If no file exists at path.

ValueError
If the file is empty, if it has no contract name, or if a field has an invalid value (see from_dict()).

Examples

Let’s write the contract file shown above to a temporary directory, then load it.

import pointblank as pb
import polars as pl
import tempfile
from pathlib import Path

contract_yaml = '''
contract:
  name: payments
  version: 1.0.0
  owner: billing-team
  schema:
    payment_id: String
    amount: Float64
  steps:
  - col_vals_not_null:
      columns: payment_id
  - col_vals_gt:
      columns: amount
      value: 0
  thresholds:
    warning: 1
'''

contract_path = Path(tempfile.mkdtemp()) / "payments.yaml"
contract_path.write_text(contract_yaml)

payments_contract = pb.Contract.from_yaml(contract_path)

payments_contract
Contract(name='payments', direction='source', version='1.0.0', schema=<defined>, steps=2)

The loaded contract works like one defined in Python. Here it is applied to a small payments table:

payments = pl.DataFrame({"payment_id": ["T1", "T2", "T3"], "amount": [50.0, 0.0, 12.5]})

payments_contract.validate(payments)
Pointblank Validation
Contract: payments v1.0.0
PolarspaymentsWARNING1ERROR—CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
○ — — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
payment_id — ✓ 3 3
1.00
0
0.00
○ — — —
#AAAAAA 3
col_vals_gt
col_vals_gt()
amount 0 ✓ 3 2
0.67
1
0.33
● — —
Owner: billing-teamVersion: 1.0.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 payment_id String 1 payment_id ✓ String ✓
2 amount Float64 2 amount ✓ Float64 ✓
Supplied Column Schema:
[('payment_id', 'String'), ('amount', 'Float64')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The report label, Contract: payments v1.0.0, comes from the file. The payment with an amount of 0.0 fails the col_vals_gt step and triggers the warning threshold.

to_dict()

Convert the contract to a dictionary.

Usage

Source

to_dict()

This method gives you the contract as plain Python data (strings, numbers, lists, and dictionaries), ready to be written as JSON, stored in a database or configuration system, or compared with another contract’s definition. It’s the same structure that to_yaml() writes, but without the top-level contract key. from_dict() turns it back into a Contract.

The dictionary always has a "name" key. Other fields are only included when they are set. direction= is left out when it’s "source" and on_violation= when it’s "warn", because those are the defaults. The schema becomes a mapping of column names to type strings, each step becomes a single-key mapping from method name to arguments (see Step.to_dict()), and the thresholds become a mapping with the levels that are set.

Returns

dict[str, Any]
A dictionary representation of the contract.

Examples

Let’s create a contract for a target table and convert it to a dictionary.

import pointblank as pb

accounts_contract = pb.Contract(
    name="accounts",
    direction="target",
    version="1.0.0",
    schema=pb.Schema(account_id="Int64", email="String"),
    steps=[
        pb.Step("rows_distinct", columns_subset="account_id"),
        pb.Step("col_vals_regex", columns="email", pattern=r"^[^@]+@[^@]+$"),
    ],
    thresholds=pb.Thresholds(warning=0.01),
)

accounts_contract.to_dict()
{'name': 'accounts',
 'direction': 'target',
 'version': '1.0.0',
 'schema': {'account_id': 'Int64', 'email': 'String'},
 'steps': [{'rows_distinct': {'columns_subset': 'account_id'}},
  {'col_vals_regex': {'columns': 'email', 'pattern': '^[^@]+@[^@]+$'}}],
 'thresholds': {'warning': 0.01}}

direction is included because "target" isn’t the default, while on_violation, owner, and the other unset fields are left out. Only the warning level of the thresholds is set, so it’s the only one listed.

A dictionary like this converts directly to JSON with the standard library:

import json

print(json.dumps(accounts_contract.to_dict(), indent=2))
{
  "name": "accounts",
  "direction": "target",
  "version": "1.0.0",
  "schema": {
    "account_id": "Int64",
    "email": "String"
  },
  "steps": [
    {
      "rows_distinct": {
        "columns_subset": "account_id"
      }
    },
    {
      "col_vals_regex": {
        "columns": "email",
        "pattern": "^[^@]+@[^@]+$"
      }
    }
  ],
  "thresholds": {
    "warning": 0.01
  }
}

The JSON text can be stored in a database column or sent to an API. After it’s parsed with json.loads(), from_dict() rebuilds the contract.

from_dict()

Create a contract from a dictionary.

Usage

Source

from_dict(data)

This is the reverse of to_dict(). Use it when the contract comes from somewhere other than a YAML file: a JSON document, an API response, a database record, or a dictionary built in code. To read a YAML file directly, use from_yaml().

The dictionary uses the contract’s parameter names as keys. Only "name" is required. "schema" should map column names to type strings, "steps" should be a list of single-key mappings from method name to arguments, and "thresholds" should map any of "warning", "error", and "critical" to a value. The parsed contract is checked as usual, so an invalid direction, on_violation, or step method raises an error.

Parameters

data: dict[str, Any]
A dictionary describing a contract, in the form produced by to_dict().

Returns

Contract
A new Contract object.

Raises

ValueError

If "name" is missing or empty, if "direction" or "on_violation" has an invalid value, or if a step names a method that contracts don’t support.

TypeError
If "steps" isn’t a list, or if a step’s arguments aren’t a dictionary.

Examples

Suppose a contract definition arrives as JSON from a configuration service. After parsing, it’s a dictionary that from_dict() can turn into a Contract.

import json
import pointblank as pb

contract_json = '''
{
  "name": "shipments",
  "version": "0.3.0",
  "owner": "logistics",
  "schema": {"shipment_id": "String", "weight_kg": "Float64"},
  "steps": [
    {"col_vals_not_null": {"columns": "shipment_id"}},
    {"col_vals_between": {"columns": "weight_kg", "left": 0, "right": 1000}}
  ],
  "thresholds": {"warning": 1, "error": 0.05}
}
'''

shipments_contract = pb.Contract.from_dict(json.loads(contract_json))

shipments_contract
Contract(name='shipments', direction='source', version='0.3.0', schema=<defined>, steps=2)

The result is a regular Contract, ready to use on data:

import polars as pl

shipments = pl.DataFrame(
    {"shipment_id": ["S1", "S2", "S3"], "weight_kg": [12.5, 980.0, 1250.0]}
)

shipments_contract.validate(shipments)
Pointblank Validation
Contract: shipments v0.3.0
PolarsshipmentsWARNING1ERROR0.05CRITICAL—
STEP COLUMNS VALUES TBL EVAL UNITS PASS FAIL W E C EXT
#4CA64C 1
col_schema_match
col_schema_match()
— SCHEMA ✓ 1 1
1.00
0
0.00
○ ○ — —
#4CA64C 2
col_vals_not_null
col_vals_not_null()
shipment_id — ✓ 3 3
1.00
0
0.00
○ ○ — —
#EBBC14 3
col_vals_between
col_vals_between()
weight_kg [0, 1000] ✓ 3 2
0.67
1
0.33
● ● —
Owner: logisticsVersion: 0.3.0

Notes

Step 1 (schema_check) ✓ Schema validation passed.

Schema Comparison
TABLE SCHEMA
COLUMN DATA TYPE COLUMN DATA TYPE
1 shipment_id String 1 shipment_id ✓ String ✓
2 weight_kg Float64 2 weight_kg ✓ Float64 ✓
Supplied Column Schema:
[('shipment_id', 'String'), ('weight_kg', 'Float64')]
Schema Match Settings
COMPLETE
IN ORDER
COLUMN ≠ column
DTYPE ≠ dtype
float ≠ float64

The schema check and the not-null check pass, and the range check flags the shipment weighing 1250 kg.