10
\$\begingroup\$

I wrote a simple Python IBAN validator, which I'd like some reviews on.

INTRO

Quoting from wikipedia, we know that:

The International Bank Account Number (IBAN) is an internationally agreed system of identifying bank accounts across national borders to facilitate the communication and processing of cross border transactions with a reduced risk of transcription errors.

Now, an IBAN may look like this: GB82WEST12345698765432

There are three algorithms to validate an IBAN:

  1. Validating the IBAN
  2. Generating IBAN check digits
  3. Modulo operation on IBAN

I used only the first 2 methods in my program.


1. Validating the IBAN

Description (from wiki):

An IBAN is validated by converting it into an integer and performing a basic mod-97 operation (as described in ISO 7064) on it. If the IBAN is valid, the remainder equals 1. The algorithm of IBAN validation is as follows:

Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid Move the four initial characters to the end of the string Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35 Interpret the string as a decimal integer and compute the remainder of that number on division by 97 If the remainder is 1, the check digit test is passed and the IBAN might be valid.

Example (fictitious United Kingdom bank, sort code 12-34-56, account number 98765432):

• IBAN:                   GB82WEST12345698765432  
• Rearrange:              WEST12345698765432GB82  
• Convert to integer:     3214282912345698765432161182    
• Compute remainder:      3214282912345698765432161182    mod 97 = 1

2. Generating IBAN check digits

Description (from wiki):

According to the ECBS "generation of the IBAN shall be the exclusive responsibility of the bank/branch servicing the account".[8] The ECBS document replicates part of the ISO/IEC 7064:2003 standard as a method for generating check digits in the range 02 to 98. Check digits in the ranges 00 to 96, 01 to 97, and 03 to 99 will also provide validation of an IBAN, but the standard is silent as to whether or not these ranges may be used.

The preferred algorithm is:

  1. Check that the total IBAN length is correct as per the country.If not, the IBAN is invalid. (I skipped this)
  2. Replace the two check digits by 00 (e.g. GB00 for the UK)
  3. Move the four initial characters to the end of the string
  4. Replace the letters in the string with digits, expanding the string as necessary, such that A or a = 10, B or b = 11, and Z or z = 35. Each alphabetic character is therefore replaced by 2 digits
  5. Convert the string to an integer (i.e. ignore leading zeroes)
  6. Calculate mod-97 of the new number, which results in the remainder > 7. Subtract the remainder from 98, and use the result for the two check digits. If the result is a single digit number, pad it with a leading 0 to make a two-digit number

Code (Python 3)

letter_dic = {"A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, "G": 16, "H": 17, "I": 18, "J": 19, "K": 20,
              "L": 21, "M": 22, "N": 23, "O": 24, "P": 25, "Q": 26, "R": 27, "S": 28, "T": 29, "U": 30, "V": 31,
              "W": 32, "X": 33, "Y": 34, "Z": 35,
              "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9}

letters = {ord(k): str(v) for k, v in letter_dic.items()}


def chech_validation_chars_iban(iban):
    zeros_iban = iban[:2] + '00' + iban[4:]
    iban_inverted = zeros_iban[4:] + zeros_iban[:4]
    iban_numbered = iban_inverted.translate(letters)

    verification_chars = 98 - (int(iban_numbered) % 97)

    if verification_chars < 10:
        verification_chars = '{:02}'.format(int(verification_chars))
    return verification_chars


def validate_iban(iban):
    iban_inverted = iban[4:] + iban[:4]
    iban_numbered = iban_inverted.translate(letters)

    return int(iban_numbered) % 97


if __name__ == '__main__':
    my_iban = 'RO13RZBR0000060007134800'

    if chech_validation_chars_iban(my_iban) == int(my_iban[2:4]):
        if validate_iban(my_iban) == 1:
            print('IBAN ok!\n')
        else:
            print('IBAN nok!\n')
    else:
        print('IBAN nok!\n')

Any advice on what could go wrong, tips or improvements are welcome!

\$\endgroup\$
11
\$\begingroup\$

Changes I'd make, from the top down:

  • Use string with enumerate to make the dictionary. Rather than manually making one.
  • letters is a constant, so use LETTERS.
  • Make a new function to change the layout of an IBAN and change it to a number.
  • Rename chech_validation_chars_iban to the name generate_iban_check_digits as it's not a check, it's a transform.
  • Change the format to use strings, {:0>2}, and remove the if.
  • Change validate_iban to check if it's a valid IBAN, and so return a boolean.
  • Use and to reduce the amount of ifs.

This resulted in:

import string
LETTERS = {ord(d): str(i) for i, d in enumerate(string.digits + string.ascii_uppercase)}


def _number_iban(iban):
    return (iban[4:] + iban[:4]).translate(LETTERS)


def generate_iban_check_digits(iban):
    number_iban = _number_iban(iban[:2] + '00' + iban[4:])
    return '{:0>2}'.format(98 - (int(number_iban) % 97))


def valid_iban(iban):
    return int(_number_iban(iban)) % 97 == 1


if __name__ == '__main__':
    my_iban = 'RO13RZBR0000060007134800'
    if generate_iban_check_digits(my_iban) == my_iban[2:4] and valid_iban(my_iban):
        print('IBAN ok!\n')
    else:
        print('IBAN not ok!\n')

There is one glairing problem with this, the specification states that it allows both upper and lowercase input:

Replace the letters in the string with digits, expanding the string as necessary, such that A or a = 10, B or b = 11, and Z or z = 35. Each alphabetic character is therefore replaced by 2 digits

However we only allow uppercase. To amend this I'd change the letters dict to include the lowercase letters. But makes the code a little harder to read, and not as clean. And so the cleanest way would probably be to use itertools.chain, with the method we're using now.

_LETTERS = chain(enumerate(string.digits + string.ascii_uppercase),
                 enumerate(string.ascii_lowercase, 10))
LETTERS = {ord(d): str(i) for i, d in _LETTERS}
\$\endgroup\$
  • \$\begingroup\$ Instead of return '{:0>2}'.format(98 - (int(iban_numbered) % 97)) you meant return '{:0>2}'.format(98 - (int(number_iban) % 97)), right ? Thanks for the answer, it's awesome. \$\endgroup\$ – Grajdeanu Alex. Jul 20 '16 at 8:41
  • 1
    \$\begingroup\$ @Dex'ter Yeah, I don't know why I didn't change both of them... Silly me \$\endgroup\$ – Peilonrayz Jul 20 '16 at 8:48
0
\$\begingroup\$

You can simplify the letter to digit conversion using regex and lambda function:

re.sub('[A-Z]', lambda m: str(ord(m.group()) - 55), (iban[4:] + iban[:4]))

Translate replaces characters by one digit AFAIK. Therefore might not be the optimal choice for this IBAN use-case.

\$\endgroup\$
  • 2
    \$\begingroup\$ What do you mean by "Translate replaces characters by one digit"? 'A'.translate(LETTERS) -> '10' for me. \$\endgroup\$ – Peilonrayz Jun 2 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.