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.
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:
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=
if the contract has a schema, a col_schema_match() step is added first
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:
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.
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 pbimport polars as plorders_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
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.
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.
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.
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().
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.
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.
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.
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.
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.
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".
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.
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:
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()).
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:
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.
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.
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:
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().
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.