# How to parse UK iXBRL company accounts in Python

Install ixbrlparse (version 0.11.2 tested here), open the file with IXBRL.open, then pick each figure by its FRC concept name, its period (the balance sheet date, or the year ending on it) and its dimensions (none, for totals). Skip nil facts, which the library reads as zero, and check scale and sign on anything that looks odd.

Everything below was run, not just written. We tested the script against 54 real filings from the Companies House Accounts Data Product, with balance sheet dates from 2009 to 2026 and taxonomies ranging from a 2008 Companies House schema to the 2026 FRC suite, and against 24 group accounts from the daily file published on 3 October 2026. The pitfalls section lists what those files actually contain, including a scale error in about one in nine of the filings that tag a head count.

## What is in a UK iXBRL accounts file?

An iXBRL file is an XHTML page you can open in a browser. Hidden in its markup, each tagged figure is wrapped in an `ix:nonFraction` tag and each piece of tagged text in an `ix:nonNumeric` tag. A numeric fact carries:

- **a concept name**, such as `core:TurnoverRevenue`, from the taxonomy the file references;
- **a context**, which names the company, the period (an instant such as the balance sheet date, or a start and end date) and, optionally, dimension members that break the figure down;
- **a unit**, usually `iso4217:GBP` for money or `xbrli:pure` for counts;
- **presentation attributes**: `format` (how the displayed text is written), `scale` (a power of ten), `sign` (whether the value is the negative of what is displayed) and `decimals`.

You can get files in bulk from the Accounts Data Product, or one company at a time from the Document API by asking for `application/xhtml+xml`. [How to get UK company financial data through an API](https://companystack.co.uk/guides/how-to-get-uk-company-financial-data-via-api) covers both routes.

## Which Python library should you use?

**ixbrlparse** is a small, MIT-licensed library that reads iXBRL and older XBRL files into Python objects and applies the format, scale and sign rules for you. It does not load the taxonomy, so it gives you concept names, not labels, and it does not validate. We tested version 0.11.2 on Python 3.10.12, with beautifulsoup4 4.15.0 and lxml 6.1.3.

**Arelle** is an open-source XBRL platform under the Apache 2.0 licence that loads the full taxonomy, validates filings and supports Inline XBRL 1.1. Use it when you need labels, calculation checks or validation; it is heavier, and it fetches taxonomy files.

```bash
python3 -m venv .venv
. .venv/bin/activate
pip install ixbrlparse==0.11.2
```

## How do you take a first look at the facts?

This prints turnover and creditors from a 2025 filing in the daily Accounts Data Product (company 00645715, chosen because it includes a profit and loss account).

```python
from ixbrlparse import IXBRL

doc = IXBRL.open("Prod223_4316_00645715_20251231.html")
print(doc.schema)
for fact in doc.numeric:
    if fact.name in ("TurnoverRevenue", "Creditors"):
        ctx = fact.context
        period = ctx.instant or f"{ctx.startdate} to {ctx.enddate}"
        members = [s["value"] for s in ctx.segments]
        print(fact.name, period, fact.value, fact.unit, members)
```

```text
https://xbrl.frc.org.uk/FRS-102/2024-01-01/FRS-102-2024-01-01.xsd
TurnoverRevenue 2025-01-01 to 2025-12-31 3221180.0 iso4217:GBP []
TurnoverRevenue 2024-01-01 to 2024-12-31 3229496.0 iso4217:GBP []
Creditors 2025-12-31 896278.0 iso4217:GBP ['core:CurrentFinancialInstruments', 'core:WithinOneYear']
Creditors 2024-12-31 1029344.0 iso4217:GBP ['core:CurrentFinancialInstruments', 'core:WithinOneYear']
Creditors 2025-12-31 2914374.0 iso4217:GBP ['core:Non-currentFinancialInstruments', 'core:AfterOneYear']
Creditors 2024-12-31 2901370.0 iso4217:GBP ['core:Non-currentFinancialInstruments', 'core:AfterOneYear']
Creditors 2025-12-31 896278.0 iso4217:GBP ['core:CurrentFinancialInstruments', 'core:WithinOneYear']
Creditors 2024-12-31 1029344.0 iso4217:GBP ['core:CurrentFinancialInstruments', 'core:WithinOneYear']
```

`fact.name` is the concept's local name (the library keeps the prefix in `fact.schema`), and `fact.value` is the number with scale and sign already applied. Note what the output shows: turnover is a duration, creditors are instants, the creditors are split by dimension members, and the same creditors figure appears twice because it is tagged in two places in the document.

## How do periods, contexts and dimensions work?

**Periods.** Balance sheet items are instants at the balance sheet date; profit and loss items are durations ending on it. An instant written as a date means the end of that day, so last year's balance sheet sits at the day before this year's period starts: 31 December 2024 for a year that starts on 1 January 2025.

**Dimensions.** A dimension breaks a concept down: creditors by when they fall due, equity by reserve, fixed assets by class. The FRC's tagging guide says most dimensions have a default member that applies when no member is given, which for breakdowns is the total. In practice the headline total is the fact with no dimension at all.

**Group and company.** The FRC defines `Consolidated` and `Company [default]` members to separate a group's figures from the parent company's, with company as the default. A parent's own balance sheet is therefore the undimensioned fact, and the group's is the fact with the `Consolidated` member (in the business taxonomy, dimension `GroupCompanyDataDimension`).

So to read a headline figure you match three things: the concept name, the period, and the exact set of dimension members.

## The complete script

The script below reads the balance sheet date, finds the prior year, and returns twelve headline figures for both years, on the company basis or, with `--group`, the consolidated one. It skips nil facts, refuses duplicates that disagree, understands the three ways creditors are tagged, falls back to older UK GAAP names, and flags results that need a human look.

```python
"""Read headline figures from a UK iXBRL (or older XBRL) accounts file.

Tested on Python 3.10.12 with ixbrlparse 0.11.2, beautifulsoup4 4.15.0
and lxml 6.1.3. Usage: python accounts_figures.py accounts.html [--group]
"""
import json
import sys
from collections import defaultdict
from datetime import date, timedelta

from ixbrlparse import IXBRL

# Concepts by local name: FRC taxonomy names first, then the older
# UK GAAP names used in filings made before FRS 102.
CONCEPTS = {
    "turnover": ["TurnoverRevenue"],
    "profit_before_tax": ["ProfitLossOnOrdinaryActivitiesBeforeTax"],
    "profit_after_tax": ["ProfitLoss"],
    "fixed_assets": ["FixedAssets"],
    "current_assets": ["CurrentAssets"],
    "cash": ["CashBankOnHand", "CashBankInHand"],
    "net_current_assets": ["NetCurrentAssetsLiabilities"],
    "net_assets": ["NetAssetsLiabilities", "NetAssetsLiabilitiesIncludingPensionAssetLiability"],
    "equity": ["Equity", "ShareholderFunds"],
    "employees": ["AverageNumberEmployeesDuringPeriod"],
}
# FRC taxonomies tag creditors as one concept, Creditors, split by dimension
# members: a maturity member, a current/non-current member, or both.
# UK GAAP filings used separate concepts instead.
WITHIN = ("MaturitiesOrExpirationPeriodsDimension", "WithinOneYear")
AFTER = ("MaturitiesOrExpirationPeriodsDimension", "AfterOneYear")
CURRENT = ("FinancialInstrumentCurrentNon-currentDimension", "CurrentFinancialInstruments")
NON_CURRENT = ("FinancialInstrumentCurrentNon-currentDimension", "Non-currentFinancialInstruments")
# Group accounts tag consolidated figures with this member; company figures
# are the default and carry no group/company dimension at all.
GROUP = ("GroupCompanyDataDimension", "Consolidated")
CREDITORS = {
    "creditors_within_one_year": (
        [{WITHIN}, {CURRENT}, {CURRENT, WITHIN}],
        ["CreditorsDueWithinOneYear", "CreditorsDueWithinOneYearTotalCurrentLiabilities"],
    ),
    "creditors_after_one_year": (
        [{AFTER}, {NON_CURRENT}, {NON_CURRENT, AFTER}],
        ["CreditorsDueAfterOneYear", "CreditorsDueAfterOneYearTotalNoncurrentLiabilities"],
    ),
}


def local(qname):
    """'core:Equity' -> 'Equity'. Prefixes vary between filings (core:, uk-core:, ns5:)."""
    return qname.rsplit(":", 1)[-1]


def load(path):
    doc = IXBRL.open(path, raise_on_error=False)
    facts = defaultdict(set)  # (concept, period, dimensions) -> values seen
    for fact in doc.numeric:
        ctx = fact.context
        if isinstance(ctx, str) or fact.value is None:
            continue  # no matching context, or a format that carries no number
        if fact.soup_tag.get("xsi:nil") == "true":
            continue  # nil means "not reported"; ixbrlparse would read it as 0
        dims = frozenset(
            (local(s["dimension"]), local(s["value"]))
            for s in ctx.segments or []
            if s.get("dimension")
        )
        period = ctx.instant or (ctx.startdate, ctx.enddate)
        facts[(local(fact.name), period, dims)].add(round(fact.value, 6))  # drop float noise
    return doc, facts


def text_fact(doc, name):
    for item in doc.nonnumeric:
        if local(item.name) == name and item.value:
            return item.value
    return None


def balance_sheet_date(doc, facts):
    """The tagged BalanceSheetDate if it reads as a date, else the latest instant."""
    tagged = text_fact(doc, "BalanceSheetDate")
    if isinstance(tagged, date):
        return tagged
    try:
        return date.fromisoformat(tagged)
    except (TypeError, ValueError):
        return max(p for (_, p, _) in facts if isinstance(p, date))


def prior_date(facts, current):
    """The day before the current period starts (an XBRL instant is the end of that day)."""
    starts = [p[0] for (_, p, _) in facts if isinstance(p, tuple) and p[1] == current]
    if starts:
        return min(starts) - timedelta(days=1)
    earlier = [p for (_, p, _) in facts if isinstance(p, date) and p < current]
    return max(earlier) if earlier else None


def value(facts, names, when, dims=frozenset()):
    """A concept at an instant, or for the longest period ending on that date."""
    for name in names:
        matches = {}
        for (concept, period, d), values in facts.items():
            end = period if isinstance(period, date) else period[1]
            if concept == name and d == dims and end == when:
                matches[period] = values
        if matches:
            longest = max(matches, key=lambda p: 0 if isinstance(p, date) else (p[1] - p[0]).days)
            values = matches[longest]
            if len(values) > 1:
                raise ValueError(f"{name} at {when}: duplicate facts disagree {sorted(values)}")
            return next(iter(values))
    return None


def creditors(facts, when, splits, legacy, basis):
    for dims in splits:
        found = value(facts, ["Creditors"], when, frozenset(dims) | basis)
        if found is not None:
            return found
    return value(facts, legacy, when, basis)


def figures(facts, when, group=False):
    basis = frozenset({GROUP}) if group else frozenset()
    row = {key: value(facts, names, when, basis) for key, names in CONCEPTS.items()}
    for key, (splits, legacy) in CREDITORS.items():
        row[key] = creditors(facts, when, splits, legacy, basis)
    if row["net_assets"] is None and not group:
        row["net_assets"] = row["equity"]  # a company's total equity equals its net assets
    return row


def checks(row):
    problems = []
    staff = row["employees"]
    if staff is not None and staff != int(staff):
        problems.append(f"employees is {staff}: not a whole number, so check the scale attribute")
    if None not in (row["net_assets"], row["equity"]) and row["net_assets"] != row["equity"]:
        problems.append("net assets and equity differ: look for non-controlling interests")
    return problems


if __name__ == "__main__":
    group = "--group" in sys.argv
    path = next(arg for arg in sys.argv[1:] if arg != "--group")
    doc, facts = load(path)
    current = balance_sheet_date(doc, facts)
    prior = prior_date(facts, current)
    result = {
        "company_number": text_fact(doc, "UKCompaniesHouseRegisteredNumber"),
        "taxonomy": doc.schema,
        "basis": "group" if group else "company",
        "balance_sheet_date": str(current),
        "current": figures(facts, current, group),
        "prior": figures(facts, prior, group) if prior else None,
        "parse_errors": len(doc.errors),
    }
    result["checks"] = checks(result["current"])
    print(json.dumps(result, indent=2, default=str))
```

Running `python accounts_figures.py Prod223_4316_00645715_20251231.html` prints:

```json
{
  "company_number": "00645715",
  "taxonomy": "https://xbrl.frc.org.uk/FRS-102/2024-01-01/FRS-102-2024-01-01.xsd",
  "basis": "company",
  "balance_sheet_date": "2025-12-31",
  "current": {
    "turnover": 3221180.0,
    "profit_before_tax": 99070.0,
    "profit_after_tax": 70123.0,
    "fixed_assets": 11697197.0,
    "current_assets": 1681434.0,
    "cash": 8964.0,
    "net_current_assets": 785156.0,
    "net_assets": 8771622.0,
    "equity": 8771622.0,
    "employees": 20.0,
    "creditors_within_one_year": 896278.0,
    "creditors_after_one_year": 2914374.0
  },
  "prior": {
    "turnover": 3229496.0,
    "profit_before_tax": 430590.0,
    "profit_after_tax": 368901.0,
    "fixed_assets": 11830623.0,
    "current_assets": 1697215.0,
    "cash": 19540.0,
    "net_current_assets": 667871.0,
    "net_assets": 8779823.0,
    "equity": 8779823.0,
    "employees": 20.0,
    "creditors_within_one_year": 1029344.0,
    "creditors_after_one_year": 2901370.0
  },
  "parse_errors": 0,
  "checks": []
}
```

The values are floats because ixbrlparse returns floats; the script rounds them to six decimal places to remove binary noise. If you need exact amounts, rebuild them from the displayed text and the `scale` attribute with `decimal.Decimal`.

## How did we test it?

We compared the script's output for the 54 filings in CompanyStack's parser test set with reference values the test suite produces by loading each filing in Arelle with its full taxonomy and applying CompanyStack's field definitions. The filings cover FRS 102 accounts tagged with FRC suites from 2014 to 2026, FRS 105 micro-entity accounts, IFRS and Charities filings, the 2009 UK GAAP taxonomy, and plain XBRL from 2009 and 2012.

| Outcome | Figures |
|---|---|
| Matched the reference exactly | 510 |
| Not tagged in the filing (the reference records zero; the script returns `None`) | 26 |
| A total the filing does not tag (fixed assets where the filing tags only the individual asset lines, and a charity's funds, which the Charities taxonomy tags under its own concept) | 17 |
| Head counts tagged with a negative scale (the script flags all of them) | 6 |

We also ran the script with `--group` on the 24 filings in the 3 October 2026 daily file that tag at least 40 consolidated figures. It returned consolidated figures for all 24 and consolidated net assets for 21; 18 tagged net assets on both the group and the company basis, and one flagged a difference explained by non-controlling interests. None raised an error.

## What are the pitfalls?

### Nil facts read as zero

A fact marked `xsi:nil="true"` means "not reported". It has no text, and ixbrlparse turns empty text into 0. The script skips nil facts so that a missing figure stays `None`.

### Scale is usually right, but not always

`scale="3"` means the displayed number is in thousands, so `699,194` becomes 699,194,000, and ixbrlparse applies it correctly. The trouble is filings that use the wrong scale. In the daily file of 3 October 2026, 766 of the 6,774 filings that tag an average head count tag it with `scale="-2"`, and every one of those facts displays a whole number, so a strict reading turns 12 employees into 0.12. The taxonomy allows a decimal head count, so the script flags a fractional result for checking rather than correcting it.

### Sign follows the concept's label

The FRC's rule is that values are entered as positive unless the label puts a term in brackets, as in "Operating profit (loss)" or "Net current assets (liabilities)": a loss is a negative `ProfitLoss`, and net liabilities are a negative `NetAssetsLiabilities`. The `sign="-"` attribute is how a filing shows a positive number while storing a negative value, and ixbrlparse applies it. Expect the occasional filing that breaks the rule, and sanity-check signs on concepts that should never be negative.

### Prefixes and namespaces vary

The same FRC core taxonomy appears as `core:`, `uk-core:` and `ns5:` in our test files, and the namespace changes with every annual version. Match concepts and dimension members by local name, as the script's `local()` function does.

### Duplicates must agree

The same figure is often tagged more than once, for example on the balance sheet and again in a note. The FRC's tagging guide says one concept must not have different values in the same context, so the script accepts duplicates that agree and raises an error when they do not.

### Totals and lines are not always tagged

A company with only tangible fixed assets may tag the tangible assets line and never tag the fixed assets total. Some filings tag only `Equity`, not `NetAssetsLiabilities`; for a single company they are the same total, so the script falls back to equity. One charity filing in our set tagged only six numbers in the whole document. Absence in the tags is not absence in the accounts, so do not turn a missing tag into zero.

### Group figures, company figures and non-controlling interests

A group filing may tag both bases, only the consolidated figures or only the parent's, so check which exist before you choose. In one group filing we tested, `Equity` was tagged as the amount attributable to the parent's shareholders, with non-controlling interests tagged separately, so net assets and equity differ. The script does not fall back from net assets to equity on the group basis for that reason. The [guide to group accounts and company accounts](https://companystack.co.uk/guides/group-accounts-vs-company-accounts) explains which figures to rely on.

### Old taxonomies and plain XBRL

Filings before FRS 102 used the 2009 UK GAAP taxonomy, and some of the oldest electronic accounts in the archive, from 2009, are plain XBRL `.xml` files that reference a Companies House schema. The concepts are named differently (`ShareholderFunds`, `CashBankInHand`, `CreditorsDueWithinOneYear`), so the script lists them as alternatives.

### Formats, errors and zipped filings

ixbrlparse raises an error on a `format` it does not implement. Open files with `raise_on_error=False`, as the script does, and log `doc.errors`. The daily Accounts Data Product also contains a few zipped iXBRL packages (8 of the 7,859 filings on 3 October 2026), which you must unzip first.

## Which concepts hold the headline figures?

| Figure | FRC concept | Older UK GAAP concept | Notes |
|---|---|---|---|
| Turnover | `TurnoverRevenue` | | Duration; absent from most small company filings |
| Profit before tax | `ProfitLossOnOrdinaryActivitiesBeforeTax` | | Negative for a loss |
| Profit after tax | `ProfitLoss` | | Also tagged with equity dimensions in reserve movements |
| Fixed assets | `FixedAssets` | `FixedAssets` | Not always tagged as a total |
| Current assets | `CurrentAssets` | `CurrentAssets` | |
| Cash | `CashBankOnHand` | `CashBankInHand` | |
| Creditors within one year | `Creditors` with `WithinOneYear` or `CurrentFinancialInstruments` | `CreditorsDueWithinOneYear` | Members of the maturity and current or non-current dimensions |
| Net current assets | `NetCurrentAssetsLiabilities` | `NetCurrentAssetsLiabilities` | Negative for net current liabilities |
| Net assets | `NetAssetsLiabilities` | `NetAssetsLiabilitiesIncludingPensionAssetLiability` | Negative for net liabilities |
| Total equity | `Equity` | `ShareholderFunds` | Undimensioned fact is the total |
| Average employees | `AverageNumberEmployeesDuringPeriod` | | Unit `xbrli:pure`; watch the scale |

Every FRC name in the table appears in the FRC's 2025 core taxonomy schema, and the balance sheet names matched in our test filings tagged with FRC suites from 2014 to 2026.

## How do FRC taxonomy versions affect parsing?

The FRC publishes a new suite each year; the 2026 suite was published on 18 November 2025. Its accounts design document says the FRS 102 entry point serves both FRS 102 and FRS 105 accounts, which is why micro-entity filings reference an FRS 102 schema. The FRC leaves the choice of accepted versions to data collectors such as Companies House, which announced on its software filing forum in December 2025 that, from April 2026, it would accept the 2026, 2025, 2024, 2023 and 2022 suites.

The result is that one day's filings mix versions. The 7,851 iXBRL filings in the daily file of 3 October 2026 referenced the 2022 suite (19 filings), 2023 (2,123), 2024 (415), 2025 (4,293) and 2026 (1,001), through the FRS 102 entry point (7,810), IFRS (25), Charities (12) and FRS 101 (4). Never hard-code a namespace.

## How fast is it?

ixbrlparse reads the whole document with BeautifulSoup's XML parser, which took about a second per filing on our test machine, start-up included. That is fine for on-demand lookups. For a daily file of several thousand filings, or a monthly file of several gigabytes, run files in parallel across processes and store the extracted facts, not just the twelve figures, so you can add fields later without parsing again.

## How CompanyStack helps

CompanyStack has already read the figures from every year of filed accounts it holds, from iXBRL filings and, for some companies, from scanned PDF accounts read by software and checked. Each year is labelled filed, scanned or prior-year comparative, company and group figures are kept apart, and amounts stay in the currency filed. The API's accounts endpoint returns them in one request per company, as described in the [API docs](https://companystack.co.uk/docs) and on [CompanyStack for developers](https://companystack.co.uk/for/developers), and [how to get UK company financial data through an API](https://companystack.co.uk/guides/how-to-get-uk-company-financial-data-via-api) compares that with parsing the files yourself.

## Frequently asked questions

### Do I need the FRC taxonomy files to parse iXBRL accounts?

Not to read figures by concept name: the facts, their periods, dimensions and units are all in the file. You need the taxonomy for labels, calculation checks and validation, which a full XBRL processor such as Arelle loads for you.

### Why is turnover missing from most accounts?

Small companies can choose not to send their profit and loss account to Companies House, and micro-entities can send only a balance sheet. CompanyStack's analysis of the register on 3 October 2026 found that only 1.8% of the latest accounts of active companies, where those accounts are dated within the last 24 months, disclose turnover.

### Can ixbrlparse read the older XBRL accounts?

Yes. It parsed plain XBRL files from 2009 and 2012 in our tests, through the same IXBRL.open call. The concept names differ from the FRC ones, for example ShareholderFunds and CashBankInHand, so map both.

### Why does a head count come out as 0.12?

The filing tags the number 12 with scale="-2", which by the Inline XBRL rules means 0.12. It is a tagging error, and a common one: 766 of the 6,774 filings that tag a head count in the daily file of 3 October 2026 did it.

### How do I get group figures rather than the parent company's?

Consolidated figures carry the Consolidated member of the group and company dimension, while the parent's own figures carry no such dimension. Run the script with --group to read the consolidated facts.

## Sources

- [ixbrlparse on PyPI](https://pypi.org/project/ixbrlparse/)
- [Arelle (open-source XBRL platform) on GitHub](https://github.com/Arelle/Arelle)
- [XBRL International: Inline XBRL Part 1 Specification 1.1](https://www.xbrl.org/specification/inlinexbrl-part1/rec-2013-11-18/inlinexbrl-part1-rec-2013-11-18.html)
- [XBRL International: Dates in XBRL](https://www.xbrl.org/dates-in-xbrl/)
- [FRC: XBRL Tagging Guide, FRC Taxonomies 2026](https://www.frc.org.uk/documents/8912/XBRL_Tagging_Guide_-_FRC_Taxonomies_2026.pdf)
- [FRC: Accounts Taxonomies Design 2026](https://www.frc.org.uk/docs/8910/html/)
- [FRC: 2026 UK and Irish digital reporting taxonomies](https://www.frc.org.uk/library/standards-codes-policy/accounting-and-reporting/frc-taxonomies/current-uk-and-irish-digital-reporting-taxonomies/2026-uk-and-irish-digital-reporting-taxonomies/)
- [FRC: Current FRC Taxonomy Suites](https://www.frc.org.uk/library/standards-codes-policy/accounting-and-reporting/frc-taxonomies/current-frc-taxonomy-suites/)
- [Companies House software filing forum: 2026 FRC Taxonomies Update](https://xmlforum.companieshouse.gov.uk/t/2026-frc-taxonomies-update/1903)
- [Companies House: Accounts Data Product (daily files)](https://download.companieshouse.gov.uk/en_accountsdata.html)
- [GOV.UK: Prepare annual accounts: micro-entities, small and dormant companies](https://www.gov.uk/annual-accounts/microentities-small-and-dormant-companies)

---

Published by CompanyStack on 4 October 2026; last updated 4 October 2026. This guide is general information about UK rules and practice, not legal, tax or financial advice. Web page: https://companystack.co.uk/guides/parse-ixbrl-accounts-uk-python. How we write our guides: https://companystack.co.uk/guides/how-we-write-our-guides
