Skip to article

EU and UK award data · Reproducible Python workflow

EU and UK contract awards analyzed safely with Python.

Verify EU and UK contract-awards data with Python and DuckDB, join suppliers and contracts, and prevent shared values from being double counted.

Published September 1, 202625 min readBy DanielReviewed by Alexandra

Analyze government contract awards from the one-row-per-award product. Join supplier relationships only after the award measure is safe, and never allocate a shared award value to every named supplier. Keep primary_currency and value_basis beside every aggregate, because an award value, a signed-contract value, and paid government spending are not interchangeable.

The pinned August 29, 2026 government contract awards release contains 198 award rows, 217 award–supplier relationships, and 93 explicit contract rows. Only four awards name multiple suppliers. Yet summing primary_value from the compatible flat file produces £2,023,683,034.33, while counting the same values once from awards.csv produces £505,203,695.66. The flat result is £1,518,479,338.67 higher—a 300.57% overstatement.

One £100 million award names 16 suppliers and creates £1.5 billion of that excess by itself. This is a grain error, not a rounding error.

The safe £505.20 million figure is still not a spending statistic: it combines £205.97 million on Contracts Finder's award basis with £299.24 million on Find a Tender's contract basis. The comparison proves why grain matters; it does not measure UK procurement expenditure. This edition also happened to contain UK source rows only. TED completed successfully but returned zero result notices for its target publication date, so the release says nothing about whether EU procurement activity existed outside that bounded window.

The downloadable Python and DuckDB recipe verifies the release root and all four analytical CSVs, rejects changed schemas or broken relationships, reproduces the value reconciliation, and writes bounded, deterministic outputs with a provenance receipt.

Pinned release receipt · 2026-08-29One edition contains three analytical grains.
198 award groups

Observed award rows

UK rows only in this edition
Contracts Finder
105
UK award-stage rows
Find a Tender
93
UK contract-stage rows
TED
0
Award rows observed in this edition
198awards.csvOne row per award group
217award-suppliers.csvOne explicit award–supplier relationship
93contracts.csvOne explicit source contract record

awards.csv is the amount-bearing aggregation grain. The relationship and contract products preserve explicit links without changing that grain.

Government contract awards with Python: the short answer

Use this sequence:

  1. Pin a release tag and independently record the exact manifest byte count and SHA-256.
  2. Verify that manifest before trusting the file hashes declared inside it.
  3. Verify schema.json, source-health.json, awards.csv, award-suppliers.csv, contracts.csv, and the compatible contract-awards.csv before analysis.
  4. Load every source column as text. Cast a named field only inside the query that needs it.
  5. Use awards.csv for award counts and reported-value analysis: its key is award_group_id and the value appears once.
  6. Use award-suppliers.csv for one row per award–supplier relationship. Join it to awards for context, but keep shared values at award grain.
  7. Use contracts.csv only for explicit contract records emitted by the source. A missing row is not proof that no legal contract exists.
  8. Treat contract-awards.csv as a compatibility product at award × supplier grain. Do not sum award-level values from it.
  9. Group every primary_value result by both primary_currency and value_basis; publish the nonblank and parseable denominators.
  10. When value_is_shared=true, suppress supplier-level value attribution unless an official source provides allocations.
  11. Keep supplier identifiers source-local and retain their schemes. Do not merge organizations across systems from names alone.
  12. Interpret zero source rows only after checking source health, selection windows, and known gaps.
  13. Export row-level review evidence, reconciliation totals, exact input identities, query rules, and output hashes together.

Start with the free EU and UK government contract awards dataset when you need the published files. This article owns the post-download Python workflow. Use the government contract analysis guide for broader analytical design, the government contracts database guide for a cross-region lifecycle model, and the Find a Tender OCDS API guide when you need to collect source releases directly.

Pin the 2026-08-29 release

A release tag locates a dataset edition. A separately recorded digest establishes the exact bytes you intended to trust. This worked example pins the manifest before reading any declarations inside it:

release tag          2026-08-29
generated at         2026-08-30T13:34:50Z
schema version       2.0

manifest bytes       30,982
manifest SHA-256     2531eaa5333ce4ede47f8f501b970b19
                     2cfa4100ecc5178a9826585aab7caa89

The pinned manifest declares product grains, record counts, ordered fields, file identities, source runs, selection windows, aggregation warnings, licenses, and known limitations. The repository's schema documentation explains the normalized fields. Keep the edition date, manifest generated_at, source publication or update time, and your download time as separate facts.

This edition was published on August 30 for target date August 29. Weekend timing matters: a source can succeed and legitimately contribute no rows under a target-date filter. Never translate an empty bounded response into “the market had no activity.”

The exact analytical CSV inputs are:

File identities, bytes, row counts, fields and grains for the pinned award release
FileBytesRowsFieldsSHA-256Grain
awards.csv416,1121981026a0534417b259e7ef43693c061095c1c0a962e7ed213faf0c7e9c80db7833ec0One award group
award-suppliers.csv125,68521729cf5bc36865e2affe9b8f352f7ae70f56e4aff9be8296936fd86ebc485de9226cOne award–supplier relationship
contracts.csv68,8619333c32283079dff82e497cb9f63bd99fa14d51d667829d64bd25941a7b419075aeeOne explicit source contract
contract-awards.csv426,535217111f30abec2fc978b54c252bc83be1bb4f16a564f8a4c95e5125c69f32a60cba2b4Award × supplier compatibility row

The recipe rejects the release if an independently pinned root, manifest declaration, file size, file hash, schema version, exact ordered header, record count, primary key, or foreign-key relationship changes. A hash match identifies bytes. It does not prove the official source was complete or factually correct.

Install the tested Python environment

The recipe requires Python 3.10 or newer and pins DuckDB 1.5.5. DuckDB can keep all source columns as strings and cast only inside named analytical queries. (DuckDB Python API; DuckDB CSV reader)

On macOS, Linux, or WSL:

curl -fsSLO https://www.webtruffle.com/examples/government-contract-awards-python.py
curl -fsSLO https://www.webtruffle.com/examples/government-contract-awards-python-requirements.txt

python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r government-contract-awards-python-requirements.txt
python government-contract-awards-python.py

On Windows PowerShell:

Invoke-WebRequest `
  -Uri https://www.webtruffle.com/examples/government-contract-awards-python.py `
  -OutFile government-contract-awards-python.py
Invoke-WebRequest `
  -Uri https://www.webtruffle.com/examples/government-contract-awards-python-requirements.txt `
  -OutFile government-contract-awards-python-requirements.txt

py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r government-contract-awards-python-requirements.txt
python .\government-contract-awards-python.py

The default directory is government-contract-awards-2026-08-29. Reuse a verified local evidence directory and change the bounded review size explicitly when needed:

python government-contract-awards-python.py \
  --data-dir ./evidence/government-contract-awards-2026-08-29 \
  --review-limit 25

An existing file with the wrong byte count or digest causes a hard failure; the recipe does not silently replace evidence. Review output is capped so an accidental flag cannot turn the example into an unbounded personal-data or spreadsheet export.

Verify the manifest before the data files

The trust chain runs from an independent root toward declared assets:

pinned manifest bytes + SHA-256
    → parsed manifest declarations
        → schema, source-health and CSV bytes + SHA-256
            → exact headers, counts, keys and relationships
                → analytical queries and deterministic exports

Verify raw bytes before parsing JSON:

import hashlib
from pathlib import Path

def sha256_and_size(path: Path) -> tuple[str, int]:
    digest = hashlib.sha256()
    size = 0
    with path.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)
            size += len(chunk)
    return digest.hexdigest(), size

manifest_path = Path("manifest.json")
actual_hash, actual_bytes = sha256_and_size(manifest_path)
assert actual_bytes == 30_982
assert actual_hash == (
    "2531eaa5333ce4ede47f8f501b970b19"
    "2cfa4100ecc5178a9826585aab7caa89"
)

Only then accept the manifest's release contract:

import json

manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert manifest["dataset_id"] == "government-contract-awards"
assert manifest["target_date"] == "2026-08-29"
assert manifest["schema_version"] == "2.0"

Do not infer expected keys from these three illustrative assertions; the downloadable recipe verifies the actual nested asset declarations and exact ordered headers. Separating the fixed root from manifest-provided values prevents a self-consistent altered manifest and altered CSV from passing together.

Load all fields as text:

import duckdb

connection = duckdb.connect()
connection.execute("""
    CREATE VIEW awards AS
    SELECT *
    FROM read_csv(
      'awards.csv',
      header = true,
      all_varchar = true,
      auto_detect = false
    )
""")

The production recipe creates all four views only after structural checks pass. Text-first loading preserves leading zeros, identifier punctuation, ISO dates, decimal strings, JSON-in-CSV fields, booleans, and blanks without reader-inferred coercion.

Choose the table whose grain matches the question

Safe join decision mapChoose the product whose grain matches the question.
Join key · award_group_id
  1. Product 01198 rows
    awards.csv

    One award_group_id

    Count awards and aggregate primary value here, grouped by currency and value basis.

  2. Product 02217 rows
    award-suppliers.csv

    One relationship_id

    Join through award_group_id for named suppliers; keep amount aggregation at award grain.

  3. Product 0393 rows
    contracts.csv

    One contract_group_id

    Join through award_group_id only when an explicit source contract record is required.

  4. Product 04217 rows
    contract-awards.csv

    Compatibility relationship grain

    Use for row-level compatibility or inspection. Never sum shared values from this flat view.

Decision path

  1. Need award counts or value?

    Start and finish the aggregate in awards.csv.

  2. Need supplier context?

    Aggregate awards first, then join award-suppliers.csv by award_group_id.

  3. Need contract timing?

    Left join contracts.csv by award_group_id; no row means no explicit contract in this product, not proof that no contract exists.

  4. Need a flat export?

    Use contract-awards.csv for compatibility, not monetary aggregation.

Entity identity
Prefer explicit source-local identifiers. A supplier name is not a stable cross-source entity key.
Missing contract
Absence from contracts.csv means no explicit source contract record in this edition, not no contract.
Shared value
value_is_shared=true marks award value that must not be copied into supplier-level totals.

Safe pattern: verify the pinned manifest, validate each product key, aggregate awards.csv, and only then attach relationship or contract evidence for the rows the analysis needs.

The release publishes four useful but non-interchangeable products:

  • awards.csv: one normalized award row per award_group_id. Use it for award counts, lifecycle segmentation, buyer analysis, and reported-value analysis.
  • award-suppliers.csv: one relationship per relationship_id, connecting one award_group_id to one named supplier observation. Use it for winner rosters and relationship-level supplier attributes.
  • contracts.csv: one explicit source contract per contract_group_id, linked to an award. Use it for source-provided signed dates, periods, statuses, and contract values.
  • contract-awards.csv: a compatible flattened award × supplier view. It is convenient for row inspection and legacy imports, but award-level measures repeat when an award names more than one supplier.

The Open Contracting Data Standard guidance treats awards and signed contracts as different stages and permits an award to name multiple suppliers. The UK's official open contracting publication says both Contracts Finder and Find a Tender expose OCDS data. Normalization makes the sources easier to compare; it does not erase their different notice, identifier, and value semantics.

Assert the intended keys before joining:

SELECT count(*) AS rows,
       count(DISTINCT award_group_id) AS award_keys
FROM awards;
-- 198 rows, 198 keys

SELECT count(*) AS rows,
       count(DISTINCT relationship_id) AS relationship_keys,
       count(DISTINCT award_group_id) AS awards_represented
FROM award_suppliers;
-- 217 rows, 217 relationship keys, 198 awards represented

SELECT count(*) AS rows,
       count(DISTINCT contract_group_id) AS contract_keys,
       count(DISTINCT award_group_id) AS awards_represented
FROM contracts;
-- 93 rows, 93 contract keys, 93 awards represented in this edition

Also require that every relationship and contract foreign key exists in awards.csv. Do not require every award to have a contract row: the product records explicit source contracts, and source stages differ.

Why the compatible flat file inflates value

Relationship-grain inflationRepeating shared value turns £505.20M into £2.024B.
+300.57%

Aggregation-safe illustration

One primary value per award group

£505.20M

£505,203,695.66

Read from awards.csv, then kept separate by currency and value_basis.

Naïve flat-file illustration

Sum every award–supplier row

£2.024B

£2,023,683,034.33

The compatibility flat file repeats shared award value once for every named supplier relationship.

Arithmetic difference
+£1.518B
£1,518,479,338.67 repeated in relationship rows.
Relative inflation
+300.57%
The naïve result is 4.0057 times the safe arithmetic result.
Correct action
Aggregate awards first.
Join suppliers only after the award-level result is stable.

The flat file carries award context beside every supplier relationship. That is useful until an award measure is summed from relationship rows.

The exact reconciliation is:

awards.csv primary_value, once per award       £505,203,695.66
contract-awards.csv primary_value, per relation £2,023,683,034.33
                                                    -----------------
repeated relationship-row value                 £1,518,479,338.67
relative overstatement                                  300.57%

There are 197 nonblank award values and 216 nonblank flat-row values. One award lacks a primary value and remains unknown in both views. SUM ignoring nulls is not enough: always report the count of total, nonblank, and successfully parsed records beside a total.

The unsafe query is short:

SELECT sum(try_cast(primary_value AS DECIMAL(38, 2)))
FROM contract_awards;

The safer pattern selects the award grain and preserves the measure's semantics:

SELECT
  source,
  primary_currency,
  value_basis,
  count(*) AS awards,
  count(primary_value) FILTER (WHERE trim(primary_value) <> '') AS valued_awards,
  count(try_cast(nullif(trim(primary_value), '') AS DECIMAL(38, 2))) AS parseable_awards,
  sum(try_cast(nullif(trim(primary_value), '') AS DECIMAL(38, 2))) AS reported_value
FROM awards
GROUP BY source, primary_currency, value_basis
ORDER BY source, primary_currency, value_basis;

Do not “fix” the flat query with SUM(DISTINCT primary_value). Two unrelated awards can have the same amount, and that expression deduplicates values rather than awards. Do not divide by supplier_count either: equal allocation would be invented unless the official source publishes supplier-specific amounts.

Aggregate awards by currency and value basis

Source × currency × value basisGBP does not make award and contract values interchangeable.
Keep three result groups

Award stage

Contracts Finder

value_basis=award

£205.967M

£205,967,080.37

105 awards · 105 valued

Contract stage

Find a Tender

value_basis=contract

£299.237M

£299,236,615.29

92 awards · 92 valued

Primary value missing

Find a Tender

value_basis=unavailable

Unknown

No currency or primary value provided

1 award · 0 valued

01primary_currency

Separate GBP from every other reported currency.

02value_basis

Keep award, contract, and tender semantics in distinct groups.

03primary_value

Preserve null as unknown; do not silently replace it with zero.

All nonblank primary values in this edition are GBP and occupy two value bases. One additional Find a Tender award has no primary value or currency and remains in a separate unavailable group:

Award counts and reported values separated by source, currency and value basis
SourceValue basisCurrencyAwardsValuedReported value
Contracts FinderawardGBP105105£205,967,080.37
Find a TendercontractGBP9292£299,236,615.29
Find a TenderunavailableNot provided10Unknown

Keep those rows separate in downstream dashboards. The combined £505.20 million appears only as the award-grain side of the flat-file reconciliation; keep the source and basis groups separate for analysis. It must not be labeled awarded spend, contracted spend, fiscal-period expenditure, paid invoices, market size, or total government procurement.

primary_value is a convenience selector that preserves its origin in value_basis. Use the more specific award_value, contract_value, or tender_value when the question requires one concept. Retain the associated currency field and source notice. Never convert null to zero, and never convert currencies without a documented rate source, date, direction, rounding rule, and retained original measure.

Join suppliers without allocating shared value

Supplier multiplicity · 198 awardsFour multi-supplier awards create all 19 extra relationship rows.
198 → 217 rows
  1. AwardsRelationships · 194

    194

    1 supplier per award

  2. AwardsRelationships · 4

    2

    2 suppliers per award

  3. AwardsRelationships · 3

    1

    3 suppliers per award

  4. AwardsRelationships · 16

    1

    16 suppliers per award

Largest multiplier

16 suppliers

One £100M shared-value award contributes 16 relationship rows.

Raw flat sum: £1.6B. Correct award-grain value: £100M. The repeated relationships add £1.5B of duplicated value.

All four multi-supplier awards have value_is_shared=true. Treat the relationship as named-supplier evidence; do not allocate the full award value to every supplier.

The 198 awards have this supplier multiplicity:

  • 194 awards name one supplier;
  • two awards name two suppliers;
  • one award names three suppliers; and
  • one award names 16 suppliers.

Those four multi-supplier awards create all 19 rows by which supplier relationships exceed awards. Their values total £118,389,278.67 at award grain and £1,636,868,617.34 after relationship expansion. The 16-supplier, £100 million award contributes £1.5 billion—98.78% of the total excess.

value_is_shared=true identifies the four awards. Build a relationship roster without attributing the shared measure:

SELECT
  s.award_group_id,
  s.relationship_id,
  s.source,
  s.supplier_name,
  s.supplier_identifier,
  s.supplier_identifier_scheme,
  s.supplier_scale,
  a.title,
  a.buyer_name,
  a.primary_value,
  a.primary_currency,
  a.value_basis,
  a.value_is_shared,
  CASE
    WHEN lower(a.value_is_shared) = 'true' THEN NULL
    ELSE try_cast(nullif(trim(a.primary_value), '') AS DECIMAL(38, 2))
  END AS supplier_attributable_value
FROM award_suppliers AS s
JOIN awards AS a USING (award_group_id);

The output keeps the award value visible for context and emits no supplier-attributable value for shared awards. That is a conservative analytical policy, not evidence that each supplier's share was zero. If a source later publishes allocations, store them as relationship-level measures with their own basis and provenance.

The Contracts Finder API documentation describes award details that can name multiple suppliers. Its documented awarded value is not a guaranteed supplier allocation, and the v2 notes say the supplier awarded-value field is optional and defaults to null. (Contracts Finder v2 release notes) That source behavior is exactly why duplicating an award amount across winner rows is unsafe.

Supplier scale also belongs to relationship evidence. This edition has 117 SME-labelled relationships, 76 large-labelled relationships, and 24 with no scale. Do not call the blank group large or non-SME. Do not treat those 217 rows as 217 globally unique companies: identity is source-scoped, and the same organization can appear under different identifiers or text forms.

For a source-scoped winner-frequency view, count awards rather than values and keep the identifier context in the key:

SELECT
  source,
  coalesce(nullif(supplier_identifier_scheme, ''), 'source_local')
    AS identifier_scheme,
  coalesce(
    nullif(supplier_identifier, ''),
    nullif(source_supplier_id, ''),
    relationship_id
  ) AS source_local_supplier_key,
  min(supplier_name) AS observed_supplier_name,
  count(DISTINCT award_group_id) AS awards_observed
FROM award_suppliers
GROUP BY source, identifier_scheme, source_local_supplier_key
ORDER BY awards_observed DESC, source, source_local_supplier_key
LIMIT 25;

That ranking counts named relationships inside this edition; it is not a cross-source legal-entity ranking or a supplier win rate. A win rate requires a compatible denominator of bids or eligible participations, which this award product does not provide.

Treat contracts as explicit source records

contracts.csv contains 93 explicit records, all linked to Find a Tender awards in this edition. Each of those 93 awards has one emitted contract row here. That observed one-to-one shape is not a permanent schema promise.

Use a left join from awards when measuring contract coverage:

SELECT
  a.source,
  count(*) AS awards,
  count(DISTINCT c.award_group_id) AS awards_with_explicit_contracts,
  count(DISTINCT c.contract_group_id) AS explicit_contracts
FROM awards AS a
LEFT JOIN contracts AS c USING (award_group_id)
GROUP BY a.source
ORDER BY a.source;

The result is 93 explicit contracts for Find a Tender and zero emitted contract rows for the 105 Contracts Finder awards. It does not prove that Contracts Finder awards lack signed legal agreements. It says this edition did not emit a separate explicit source-contract record for them under the dataset's mapping.

OCDS separates the award decision from contract signature and implementation. Find a Tender exposes OCDS release packages under its developer documentation, while source releases and contract objects can change over time. Use contract_group_id for contract-row identity, award_group_id for the normalized link, and retain source_contract_id plus source_notice_id for traceability.

Measure source coverage before interpreting zero

This pinned award edition contains:

Contracts Finder    105 awards    124 supplier relationships
Find a Tender        93 awards     93 supplier relationships    93 contracts
TED                    0 awards      0 supplier relationships     0 contracts

All 198 observed award rows are therefore UK source records. The broader dataset includes TED result notices, but this one target-date run returned none. The TED Search API documentation defines a filtered search interface; an empty result under a publication-date query is not a census of all EU awards.

The source windows are not identical. Contracts Finder selects award releases published on the target date with a previous-day overlap. Find a Tender selects releases updated on the target date with a previous-day overlap. TED selects result notices published on the target date. These are publication and update observations, not necessarily the legal award date.

Change labels need the same restraint. In this edition, four awards are new and 194 are unchanged relative to retained normalized history. That does not mean only four awards were legally made on August 29. It means four normalized award identities were first observed in this dataset's accepted history under the published selection and comparison rules.

Before interpreting a source count, require:

  • the source-run status and atomic-release status;
  • the exact selection field, window, and timezone;
  • result and pagination counts;
  • source-specific stage mapping;
  • known gaps and retry notes; and
  • the prior accepted edition when discussing changes.

Preserve unknown values and source-local identities

Null handling and entity identity can produce subtler errors than the obvious value duplication.

Keep these rules explicit:

  • Blank primary_value is unknown, not zero. Report the one unavailable value in the denominator.
  • Blank supplier_scale is unknown, not large, non-SME, or ineligible.
  • A missing contract row is absence from this emitted product, not proof that a contract was never signed.
  • A blank country, classification, date, bid, or framework field is not a negative answer.
  • supplier_name is display text, not a stable cross-source key.
  • supplier_identifier is meaningful only with supplier_identifier_scheme and source.
  • source_supplier_id and relationship_id support source-local traceability; they do not establish a global corporate entity.
  • buyer_source_id, source_notice_id, source_award_id, source_release_id, and source_contract_id each identify different source objects.

Find a Tender uses OCDS 1.1.5 plus extensions, and OCDS identifiers have a defined package and release context. The official UK open contracting guidance notes that Find a Tender release IDs are unique within a contracting process and party IDs are unique within a release. Retain the context instead of stripping a raw ID into a supposed universal key.

If cross-source supplier resolution matters, create a separate entity-resolution layer. Preserve every source relationship, compare strong registered identifiers under their schemes, record match evidence and method, assign confidence, and keep ambiguous cases unresolved. Fuzzy name similarity alone is not enough to merge legal entities or aggregate their awards.

Export auditable results and provenance

The recipe writes seven files:

Output files from the contract awards Python recipe and the purpose of each
OutputGrain and purpose
government-contract-awards-release-check.csvOne row per verified core input with bytes, hashes, fields, rows, keys, and declared grain.
government-contract-awards-source-summary.csvOne row per source with award, relationship, explicit-contract, value-presence, and change counts.
government-contract-awards-value-reconciliation.csvAward-grain and flat-grain sums separated by source, currency, and value basis, plus repeated-value excess.
government-contract-awards-multi-supplier-review.csvBounded award-level review of shared values, supplier multiplicity, context, and source URL.
government-contract-awards-contract-coverage.csvSource-level awards, awards with an explicit contract, and contract rows without inferring legal absence.
government-contract-awards-supplier-scale.csvRelationship counts by source and published supplier scale, retaining missing as its own state.
government-contract-awards-provenance.jsonRelease root, environment, policies, validations, queries, output identities, and limitations.

CSV outputs are deterministically sorted and byte-stable for the same verified inputs and flags. The JSON receipt can include a run timestamp, so distinguish that envelope field from deterministic analytical content and recorded output hashes.

Record at least:

  • script and dependency versions;
  • release tag, generated time, manifest URL, byte count, and independent digest;
  • every input URL, byte count, digest, row count, header, and grain;
  • primary- and foreign-key assertions;
  • value-cast, null, currency, basis, and shared-value policies;
  • review limit and deterministic sort order;
  • source coverage and change counts;
  • every acceptance equation; and
  • every deterministic CSV output's row count, byte count, and SHA-256.

Write provenance last, after all other files have closed and been hashed. Do not publish an output if its receipt cannot identify the exact input bytes and query policy that produced it.

Turn the worked example into a repeatable release gate

A daily production workflow should separate release acceptance from downstream publication:

  1. Discover a candidate tag without treating a moving latest URL as evidence.
  2. Require an independently registered manifest size and digest for that tag.
  3. Download under explicit time and byte limits; never accept partial content.
  4. Verify the manifest, schema, source health, known gaps, and every required product.
  5. Stage source columns as text and require exact headers, row counts, keys, and relationships.
  6. Run source-level, value-basis, multiplicity, contract-coverage, and null checks.
  7. Quarantine an unexpected schema, duplicate key, orphan relationship, parse failure, source failure, or reconciliation mismatch.
  8. Produce deterministic analytical outputs from the accepted staging area.
  9. Write the receipt and output hashes before changing a public pointer.
  10. Publish the edition and derived views atomically—or through an idempotent sequence that cannot expose a mixture of releases.
  11. Preserve immutable evidence and a rollback pointer.
  12. Alert on semantic changes, not only crawler failures.

Do not hard-code the observed one-contract-per-award shape, all-GBP values, zero TED rows, or four multi-supplier awards as permanent source rules. They are acceptance checkpoints for this pinned worked example. Future editions may contain several currencies, several explicit contracts per award, different source mixes, or additional relationship shapes.

Choose the download, source API, or managed feed

Use the free dataset when the normalized daily products, retained source evidence, and documented release window fit the decision. The dataset page owns discovery and downloads; this article owns reproducible post-download joins and checks.

Use the official source APIs when you need a different query boundary, the original OCDS release package, deeper source fields, or direct source-system provenance. Contracts Finder documents OCDS search, record, and release endpoints in its API documentation. Find a Tender documents its OCDS API and pagination in its developer portal. TED documents its Search API; the separate TED Search API guide covers version-aware source collection. Source APIs still require pagination, retries, rate awareness, schema monitoring, deduplication, and evidence retention.

Use a managed feed when the scope requires additional jurisdictions, longer history, supplier entity resolution, custom taxonomies, enrichment, deeper documents, alert logic, service levels, or delivery into a warehouse. Define the decision and grain before adding more records. A larger flat file with unresolved semantics makes totals less trustworthy, not more complete.

For U.S. federal award summaries and obligations, use the separate US federal contract awards Python guide. USAspending award and transaction semantics are different from UK award notices and contracts; do not union them into a global value total.

Limitations and interpretation boundaries

  • This worked example is one release tagged August 29, generated and published August 30. It cannot establish seasonality, long-run supplier concentration, or source reliability.
  • The broader dataset covers selected EU and UK result or award sources. It is not a complete worldwide or government-wide contracts ledger.
  • This edition contains UK observed rows only. TED succeeded with zero target-date result notices; that is a bounded selection outcome, not evidence of no EU awards.
  • Source selection uses publication or update windows with source-specific overlap. Rows are not necessarily awards legally made on the edition date.
  • awards.csv is aggregation-safe at its declared award-group grain. That does not make its reported values spend, invoices, cash outlays, fiscal-period flows, or complete market totals.
  • primary_value can select an award, contract, or tender value. Aggregate only with primary_currency and value_basis, and prefer the specific value column when required.
  • The £505.20 million award-grain sum deliberately mixes two value bases for arithmetic reconciliation. It must not be reported as spend or market size.
  • A relationship row names a supplier associated with an award. It does not necessarily carry a supplier-specific allocation.
  • value_is_shared=true protects multi-supplier awards. Suppressing attribution is safer than inventing equal shares.
  • The contracts product contains explicit source contract objects only. Missing does not mean no legal contract exists.
  • Supplier scale and identifiers are source evidence at relationship grain. Names do not establish unique legal entities across sources.
  • Missing remains unknown. Hashes verify file identity, not upstream truth, completeness, legality, or fitness for a particular decision.

Government contract awards Python checklist

  • [ ] The release tag and independent manifest digest are pinned.
  • [ ] The manifest is verified before any declared asset identity is trusted.
  • [ ] Schema and all four CSVs match exact bytes, hashes, headers, and row counts.
  • [ ] All source fields are loaded as text before query-specific casts.
  • [ ] award_group_id, relationship_id, and contract_group_id are unique at their declared grains.
  • [ ] Every supplier relationship and contract references an accepted award.
  • [ ] Award counts and values come from awards.csv, not the compatibility flat file.
  • [ ] Every value aggregate remains split by currency and value basis.
  • [ ] Total, nonblank, and parseable value counts accompany sums.
  • [ ] Shared award values are not attributed to every supplier.
  • [ ] Missing supplier scale remains unknown.
  • [ ] Missing explicit contract rows are not interpreted as no contract.
  • [ ] Source health and selection windows accompany zero or changed counts.
  • [ ] Supplier identifiers retain their source and scheme.
  • [ ] Cross-source entity resolution is a separate evidence-bearing process.
  • [ ] Outputs are bounded, deterministically sorted, hashed, and receipted.
  • [ ] Interpretation limitations travel with every downstream extract.

Frequently asked questions

Which CSV should I use for government contract award totals?

Use awards.csv, which contains one row per award_group_id. Keep primary_currency and value_basis in the grouping, and report valued and parseable row counts. Do not label reported award or contract values as spending.

Why does contract-awards.csv total £2.024 billion?

It is an award × supplier compatibility file. Four awards name multiple suppliers, so their award-level primary_value repeats across relationship rows. The correct one-row-per-award arithmetic is £505.20 million; the £1.518 billion excess is duplicated relationship context, not additional value.

Can I divide a shared award value equally among its suppliers?

Not without source evidence. Equal division invents an allocation. Keep the value once at award grain, retain value_is_shared=true, list every relationship, and leave supplier-attributable value null until an official supplier-specific amount is available.

Does the £505.20 million safe total measure UK procurement spending?

No. It combines Contracts Finder award-basis values and 92 Find a Tender contract-basis values inside one bounded release; a separate Find a Tender row has unavailable basis, currency, and value. The arithmetic is useful for reconciling row-grain behavior, not as expenditure, cash paid, fiscal-period spending, complete market size, or all UK procurement.

Why are there 93 contracts but 198 awards?

The contract product contains explicit source contract records. This edition emitted 93 Find a Tender contracts and no separate Contracts Finder contract rows. That mapping does not prove the other awards lack signed agreements.

Does zero TED data mean there were no EU contract awards?

No. TED succeeded but returned zero result notices for this release's target-date selection. That is a quiet bounded observation, not evidence about all publication dates, notices, countries, or EU procurement activity.

Can I count SME suppliers from supplier_scale?

You can count relationship rows by the published label: this edition has 117 SME, 76 large, and 24 missing relationships. Do not convert missing to large, and do not describe relationship rows as globally unique companies without separate entity resolution.

Can I join suppliers by name across Contracts Finder and Find a Tender?

Names alone are not reliable entity keys. Retain source-scoped identifiers and schemes, normalize cautiously, use strong registered identifiers when available, record match evidence and confidence, and leave ambiguous candidates unresolved.

Should I use this guide or the government contract analysis guide?

Use this guide to verify and query the published EU and UK award release with executable Python and DuckDB. Use the broader analysis guide to design KPIs, cohorts, time boundaries, supplier concentration methods, and decision-specific analytical contracts after the data grain is safe.