validation.bic
Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
What it does
Checks that a Business Identifier Code - the "SWIFT code" of a bank - is well formed, and splits it into its four parts. `DEUTDEFF500` is Deutsche Bank (`DEUT`), Germany (`DE`), Frankfurt (`FF`), branch `500`.
FORMAT, as the ISO 20022 payment messages validate it (the 2014 edition of ISO 9362, pattern `[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?`):
For example
validate_bic(DEUTDEFF)→ valid true, normalised DEUTDEFF, institution DEUT, country DE, location FF, branch XXX, reason — an 8-character BIC is the primary office, branch XXXvalidate_bic(DEUTDEFF500)→ valid true, normalised DEUTDEFF500, institution DEUT, country DE, location FF, branch 500, reason — an 11-character BIC with a branchvalidate_bic(NEDSZAJJXXX)→ valid true, normalised NEDSZAJJXXX, institution NEDS, country ZA, location JJ, branch XXX, reason — the explicit primary office keeps its 11 characters
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def validate_bic(value: str) -> BicCheck
| value | string | a BIC in any case, optionally spaced |
| returns | BicCheck |
The type it declares, generated into your project
@dataclass(frozen=True)
class BicCheck:
"""The result of checking a BIC. Every string is null when valid is false; reason is null when it is true."""
valid: bool
#: upper case, no spaces, 8 or 11 characters as given
normalised: Optional[str]
#: the four-character business party prefix, e.g. DEUT
institution: Optional[str]
#: the two-letter country code, e.g. DE
country: Optional[str]
#: the two-character location code, e.g. FF
location: Optional[str]
#: the three-character branch code; XXX (the primary office) for an 8-character BIC
branch: Optional[str]
#: empty, bad-character, bad-length or bad-country
reason: Optional[str]
Your code names it in one line, in the file that uses it
from fune.validation.bic import validate_bic # validation.bic@^1
from .validation_bic_types import BicCheck
def _invalid(reason: str) -> BicCheck:
return BicCheck(
valid=False, normalised=None, institution=None, country=None, location=None, branch=None, reason=reason
)
def _is_upper(ch: str) -> bool:
return "A" <= ch <= "Z"
def validate_bic(value: str) -> BicCheck:
"""Check the format of a BIC and split it into institution, country,
location and branch.
Well formed is not registered: only SWIFT's directory knows whether the
code is assigned. Validators answer rather than raise.
"""
if not isinstance(value, str):
return _invalid("empty")
# Folded by hand: str.upper() is Unicode-aware and Rust's ASCII fold is not.
bic = "".join(chr(ord(ch) - 32) if "a" <= ch <= "z" else ch for ch in value if ch != " ")
if not bic:
return _invalid("empty")
if any(not (_is_upper(ch) or "0" <= ch <= "9") for ch in bic):
return _invalid("bad-character")
if len(bic) != 8 and len(bic) != 11:
return _invalid("bad-length")
if not _is_upper(bic[4]) or not _is_upper(bic[5]):
return _invalid("bad-country")
return BicCheck(
valid=True,
normalised=bic,
institution=bic[0:4],
country=bic[4:6],
location=bic[6:8],
# An 8-character BIC is the primary office, which the 11-character form spells XXX.
branch=bic[8:11] if len(bic) == 11 else "XXX",
reason=None,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add validation.bic
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./validation.bic-1.0.0-python.fune, or fetch it from a terminal with fune pull validation.bic@1.0.0:python.
The whole function, every language, is one file too: validation.bic-1.0.0.fune, 14,777 bytes, sha256 2a573b0bd05ab318b74e3aea6ddc5e1de63a1a838cce8211f58259a5ee40f76b. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before validation.bic
after — your function gets the result and the arguments, and returns the final result.
# fune: after validation.bic
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show validation.bic --steps.
# fune: step validation.bic after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| an 8-character BIC is the primary office, branch XXX | DEUTDEFF | → | valid true, normalised DEUTDEFF, institution DEUT, country DE, location FF, branch XXX, reason — |
| an 11-character BIC with a branch | DEUTDEFF500 | → | valid true, normalised DEUTDEFF500, institution DEUT, country DE, location FF, branch 500, reason — |
| the explicit primary office keeps its 11 characters | NEDSZAJJXXX | → | valid true, normalised NEDSZAJJXXX, institution NEDS, country ZA, location JJ, branch XXX, reason — |
| a UK BIC with a digit in the location | NWBKGB2L | → | valid true, normalised NWBKGB2L, institution NWBK, country GB, location 2L, branch XXX, reason — |
| lower case and spaces are normalised | nwbk gb 2l | → | valid true, normalised NWBKGB2L, institution NWBK, country GB, location 2L, branch XXX, reason — |
| a lettered branch code | DSBACNBXSHA | → | valid true, normalised DSBACNBXSHA, institution DSBA, country CN, location BX, branch SHA, reason — |
| digits in the institution prefix are allowed since ISO 9362:2014 | 1234GB2L | → | valid true, normalised 1234GB2L, institution 1234, country GB, location 2L, branch XXX, reason — |
| a digit in the country code is refused | NWBK9B2L | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-country |
| seven characters is too short | NWBKGB2 | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-length |
| nine characters is neither 8 nor 11 | NWBKGB2L1 | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-length |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| twelve characters, a SWIFTNet terminal address, is not a BIC | DEUTDEFFA500 | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-length |
| hyphens are not accepted separators | NWBK-GB-2L | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-character |
| an accented letter is a bad character | DEUTDÉFF | → | valid false, normalised —, institution —, country —, location —, branch —, reason bad-character |
| the empty string | → | valid false, normalised —, institution —, country —, location —, branch —, reason empty | |
| a non-string is empty, not an exception | 12,345,678 | → | valid false, normalised —, institution —, country —, location —, branch —, reason empty |
More from the author
- 4 letters or digits: the business party prefix (institution). Every BIC issued so far uses letters here, but ISO 9362:2014 allows digits, and rejecting them would make this stricter than the payment messages it guards. - 2 letters: the country, an ISO 3166-1 alpha-2 code (plus `XK`, which SWIFT assigned to Kosovo). - 2 letters or digits: the location. A `0` in the second place conventionally marks a test BIC; this capability does not flag it, and it is still a well-formed code. - 3 letters or digits, optional: the branch. An 8-character BIC means the primary office and is the same as the 11-character form ending `XXX`, so `branch` is `XXX` for both. `normalised` keeps the length the caller gave, because some systems require one form or the other.
A WELL-FORMED BIC IS NOT A REGISTERED ONE. Only SWIFT's BIC directory can say that the code is assigned and still active. The country letters are checked to be letters, not looked up in the ISO 3166 list, which would need data and releases of its own; pair this with a country check if that matters.
ACCEPTED INPUT: letters in either case (folded to upper case) and digits, with ASCII spaces ignored anywhere ("DEUT DE FF"). Hyphens and other punctuation are refused.
| reason | meaning | |-----------------|------------------------------------------------------| | `empty` | nothing but spaces, or not a string at all | | `bad-character` | anything other than A-Z, a-z, 0-9 and spaces | | `bad-length` | not 8 or 11 characters | | `bad-country` | characters 5 and 6 are not both letters |
The checks run in that order; the first failure is the reason. Validators answer rather than throw.
Not enforced: SWIFT's operational conventions for particular characters in the location and branch codes, which are directory policy rather than format and are not in the ISO 20022 pattern.
Sources: ISO 9362:2014 / 2022 "Banking - Banking telecommunication messages - Business identifier code (BIC)"; the ISO 20022 BICFIDec2014Identifier and AnyBICDec2014Identifier patterns (https://www.iso20022.org); SWIFT, "Business Identifier Code (BIC)" (https://www.swift.com/standards/data-standards/bic-business-identifier-code).
Files
| Path | Bytes |
|---|---|
| README.md | 2,694 |
| impl/python.py | 1,468 |
| impl/rust.rs | 2,426 |
| impl/typescript.ts | 1,529 |
| vectors.json | 3,393 |