Reliable ISBN-13 Validation

As I’ve been progressing through the exercise of parsing ISBN numbers, I discovered that the ISBN-10 numbers that I’ve been parsing were phased out in 2007! I guess that too many people were writing books around the world and they were running out of numbers. To deal with the problem, the ISBN people created something called a ISBN-13 code that is… (wait for it) … 13 digits long instead of the 10 digits in the previous standard.

After learning this shocking information, I decided that it wouldn’t be proper to have an ISBN validator that was almost 15 years out of date, so I added support for ISBN-13 validation to my practice example.

It turns out that ISBN-13 codes are validated differently than ISBN-10 codes. The ISBN-13 standard is outlined here. And, luckily, they’ve provided a nice worked example of how to generate check digits. Luckily, the mechanism is pretty straightforward and uses smaller numbers than the ISBN-10 mechanism. Essentially, this is how the mechanism works: each even digit (remember to start counting at zero!) is multiplied by 1 and each odd digit is multiplied by 3, and then all of the digits are summed.

  1. A sum of the first 12 digits is generated by multiplying all even digits by 1 and all odd digits by 3 (and then summing them)
  2. The 13th digit (or check digit) is generated by determining how much needs to be added to the sum generated in Step #1 to make it a multiple of 10.
    • For example: If the output of Step #1 is 146, the 13th digit would be 4 because 146 + 4 = 150 (which is a multiple of 10)
    • The folks at HowToGeek have written a nice explanation of what checksums are and why they are helpful in computing if you’re interested.
  3. Then, all of the digits are combined and THAT is the ISBN-13.

The nice thing about this approach is that to verify the ISBN, all that you need to do is generate the sum as outlined above (even digits *= 1, odd *= 3) for all 13 digits and then check to see if this is evenly divisible by 10. If it is, the ISBN is valid!

So, I extended the unit test class to include some ISBN-13 numbers and added a test method to verify my solution:

class TestISBNValidator(TestCase):

...

    test_data_isbn13 = {
        "123": False,
        # A valid isbn10 should not work
        "0136091814": False,
        "978-1-86197-876-9": True,
        "978-1-56619-909-4": True,
        "9781566199094": True,
        "9781566199092": False,
        "978156619909X": False,
        # Leading or trailing spaces should work
        " 9781566199094": True,
        " 9781566199094 ": True
    }

...

    def test_validate_isbn13(self):
        for (code_string, valid) in TestISBNValidator.test_data_isbn13.items():
            result = ISBNValidator.validate_isbn13(code_string=code_string)
            self.assertEqual(result, valid)

And then I wrote the validate_isbn13() method. I also upgraded the remove_dashes method that I used to have and added the capability to not only remove dashes, but also trim spaces. I renamed the method to prepare_code_string() since it wasn’t just removing dashes anymore. Once I finished these two steps, this is what my ISBN Validator class is looking like now:

class ISBNValidator:

    @staticmethod
    def prepare_code_string(code_string: str) -> str:
        retval = code_string.strip()
        retval = retval.replace("-", "")
        return retval

    @staticmethod
    def validate_isbn10(code_string: str) -> bool:
        # Remove dashes from the string
        isbn_string = ISBNValidator.prepare_code_string(code_string)

        # Reject if string is wrong length, or is not numeric (including special case for digit 10)
        if len(isbn_string) != 10 or \
                not isbn_string[0:9].isnumeric() or \
                not (isbn_string[9].isnumeric() or isbn_string[9].lower() == "x"):
            return False
        checksum = 0
        for i in range(0, 10):
            if i == 9 and isbn_string[i].lower() == "x":
                digit = 10
            else:
                digit = int(isbn_string[i])
            checksum += digit * (10 - i)
        return (checksum % 11) == 0

    @staticmethod
    def validate_isbn13(code_string: str) -> bool:
        isbn_string = ISBNValidator.prepare_code_string(code_string)
        if len(isbn_string) != 13 or not isbn_string.isnumeric():
            return False
        checksum = 0
        for (count, digit) in enumerate(isbn_string):
            # Weight of 1 for even and 3 for odd digits
            weight = 1 + ((count % 2) * 2)
            checksum += (int(digit) * weight)
        return (checksum % 10) == 0

Now, when I run my unit tests, here is what I get:

..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

All seems good, and now we have support for both ISBN-10 and ISBN-13 validation!

  1. Practicing Python: Quick ISBN-10 Validation
  2. Basic ISBN-10 Validation in Python: Part 2
  3. ISBN Validation: Adding Simple Python Unit Tests
  4. Reliable ISBN-13 Validation
  5. ISBN Validator: Making it General Purpose
  6. Simple ISBN-10 to ISBN-13 Conversion
  7. Testing Exceptions in Python with unittest
  8. Using Simple Code Coverage in Python

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top