From 9625092499022563f8803f3271480ba540d42b1a Mon Sep 17 00:00:00 2001 From: yashbudhia Date: Sat, 5 Sep 2026 14:46:16 +0530 Subject: [PATCH] Add Indian Financial System Code (IFSC) bank branch code validation The IFSC is the 11 character code the Reserve Bank of India assigns to every bank branch on NEFT, RTGS and IMPS, and is needed for every domestic electronic transfer and for routing UPI payments. It is a 4 letter bank code, a reserved fifth character that is always 0, and a 6 character alphanumeric branch code, with no check digit. The module provides compact(), validate(), is_valid() and info() (bank and branch code). The doctests cover 60 real codes from the openly maintained Razorpay IFSC dataset, which is derived from the RBI's published list, and were chosen to span many banks and both numeric and alphanumeric branch codes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ZJQzxtgWvkWobxsDcuLoW --- stdnum/in_/ifsc.py | 102 +++++++++++++++++++++++++ tests/test_in_ifsc.doctest | 148 +++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 stdnum/in_/ifsc.py create mode 100644 tests/test_in_ifsc.doctest diff --git a/stdnum/in_/ifsc.py b/stdnum/in_/ifsc.py new file mode 100644 index 00000000..1254909e --- /dev/null +++ b/stdnum/in_/ifsc.py @@ -0,0 +1,102 @@ +# ifsc.py - functions for handling Indian IFSC bank branch codes +# +# Copyright (C) 2026 Yash Budhia +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see . + +"""IFSC (Indian Financial System Code). + +The Indian Financial System Code (IFSC) is an 11 character alphanumeric code +that the Reserve Bank of India (RBI) assigns to every bank branch that takes +part in the NEFT, RTGS and IMPS electronic funds transfer systems. It is +required for every domestic electronic transfer in India and is also used to +route UPI payments. + +The code consists of a 4 letter bank code, a fifth character that is reserved +and always 0, and a 6 character branch code. The branch code is often numeric +but may contain letters, which is common for cooperative and other sub-member +banks that transact through a sponsor bank. There is no check digit. + +More information: + +* https://en.wikipedia.org/wiki/Indian_Financial_System_Code +* https://www.rbi.org.in/Scripts/IFSCMICRDetails.aspx +* https://github.com/razorpay/ifsc + +>>> validate('SBIN0000001') +'SBIN0000001' +>>> validate('hdfc0cabblm') +'HDFC0CABBLM' +>>> validate('SBIN000001') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('SB1N0000001') # bank code must be letters +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('SBIN1000001') # fifth character is reserved and must be 0 +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> info('ICIC0000001')['bank_code'] +'ICIC' +""" + +from __future__ import annotations + +import re + +from stdnum.exceptions import * +from stdnum.util import clean + + +_ifsc_re = re.compile(r'^[A-Z]{4}[A-Z0-9]{7}$') + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace.""" + return clean(number, ' -').upper().strip() + + +def validate(number: str) -> str: + """Check if the number provided is a valid IFSC. This checks the length + and formatting.""" + number = compact(number) + if len(number) != 11: + raise InvalidLength() + if not _ifsc_re.match(number): + raise InvalidFormat() + if number[4] != '0': + raise InvalidComponent() + return number + + +def is_valid(number: str) -> bool: + """Check if the number provided is a valid IFSC. This checks the length + and formatting.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def info(number: str) -> dict[str, str]: + """Provide information that can be decoded from the IFSC.""" + number = compact(number) + return { + 'bank_code': number[:4], + 'branch_code': number[5:], + } diff --git a/tests/test_in_ifsc.doctest b/tests/test_in_ifsc.doctest new file mode 100644 index 00000000..36d9a41f --- /dev/null +++ b/tests/test_in_ifsc.doctest @@ -0,0 +1,148 @@ +test_in_ifsc.doctest - more detailed doctests for stdnum.in_.ifsc module + +Copyright (C) 2026 Yash Budhia + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see . + + +This file contains more detailed doctests for the stdnum.in_.ifsc module. It +tries to cover more corner cases and detailed functionality that is not +really useful as module documentation. + +>>> from stdnum.in_ import ifsc +>>> from stdnum.exceptions import * + +These codes come from the openly maintained Razorpay IFSC dataset, which is +derived from the list the RBI publishes, and should all be valid. They were +picked to cover many different banks and both numeric and alphanumeric branch +codes. + +>>> numbers = ''' +... +... SBIN0000001 +... HDFC0000001 +... ICIC0000001 +... UTIB0000001 +... PUNB0000100 +... ABHY0065001 +... ADCC0000001 +... AIRP00000SC +... AKJB0000001 +... ALLA0888888 +... AMCB0RTGS4S +... ANDB0TRESUY +... APBL0000001 +... APGB0000001 +... APGV0000001 +... APMC0000001 +... ASBL0000001 +... AUBL0000001 +... AUCB0000001 +... BACB0000001 +... BARB0BGGBXX +... BARC0INBB01 +... BBKM0000001 +... BCBM0000001 +... BCEY0CHEN01 +... BDBL0000001 +... BKDN0RTGSSC +... BKID000DCBL +... BNPA0RTGSMI +... BOFA0MM6205 +... BOTM0003611 +... CBIN0281102 +... CCBL0209003 +... CHAS0INBX01 +... CITI0RTGSMI +... CIUB0000082 +... CLBL0000001 +... CNRB0RTGS01 +... COAS0000001 +... CORP0000920 +... COSB0000025 +... CRLY0000001 +... CRUB0000001 +... CSBK0000001 +... CSBX0CSB001 +... DBSS0IN0811 +... DCBL0000037 +... DEOB0000001 +... DEUT0000PBC +... DLSC0000001 +... DLXB0000146 +... DMKJ0000001 +... DNSB0000001 +... DOHB0000001 +... EBIL0000001 +... ESFB0000001 +... ESMF0000001 +... FDRL0000379 +... FINO0000001 +... FSFB0000001 +... +... ''' +>>> [x for x in numbers.splitlines() if x and not ifsc.is_valid(x)] +[] + + +The fifth character is reserved and must always be 0. + +>>> ifsc.validate('SBIN1000001') +Traceback (most recent call last): + ... +InvalidComponent: ... +>>> ifsc.is_valid('SBINA000001') +False + + +The bank code is four letters and the branch code is six letters or digits. + +>>> ifsc.validate('SB1N0000001') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> ifsc.validate('SBIN00000#1') +Traceback (most recent call last): + ... +InvalidFormat: ... + + +The code is always exactly 11 characters long. + +>>> ifsc.validate('SBIN000001') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> ifsc.validate('SBIN00000012') +Traceback (most recent call last): + ... +InvalidLength: ... + + +Lower case, surrounding whitespace and separators are normalised away. + +>>> ifsc.compact(' hdfc0cabblm ') +'HDFC0CABBLM' +>>> ifsc.validate('icic0000001') +'ICIC0000001' +>>> ifsc.validate('UTIB 0000001') +'UTIB0000001' + + +The code splits into a bank code and a branch code. + +>>> ifsc.info('UTIB0000001') == {'bank_code': 'UTIB', 'branch_code': '000001'} +True +>>> ifsc.info('HDFC0CABBLM')['branch_code'] +'CABBLM'