ISBN Validator: Making it General Purpose

Now that we’ve constructed this ISBN validator to understand both ISBN-10 and ISBN-13 numbers, I thought it might be a good idea to make a more generic validate_isbn method that attempts to detect which type of ISBN we are dealing with and then calls the correct method to validate it. That way, anybody using our validator can just pass in a string and we’ll do our best to figure it out. This might be useful in a library where there are books that feature each type of ISBN.

Rather than get super complex with the logic, I decided to take a relatively simplistic approach — namely removing the spaces and dashes from the string and then checking to see if it had a length of 10 or 13. This is what it looks like in the code I built:

class ISBNValidator:

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

...

    @staticmethod
    def validate_isbn(code_string: str) -> bool:
        # Validate an ISBN of unknown format.
        isbn_string = ISBNValidator.prepare_code_string(code_string)
        if len(isbn_string) == 10:
            return ISBNValidator.validate_isbn10(isbn_string)
        elif len(isbn_string) == 13:
            return ISBNValidator.validate_isbn13(isbn_string)
        else:
            return False

I also updated the unit test suite to accommodate this change, building a new test method to test the new general purpose validate_isbn() logic. While I was doing this, I realized that I could use the two existing dictionaries of ISBN test data as input for this method, with one modification. You may notice that the ISBN 0136091814 appears in both sets of data. It is a valid ISBN-10 number, so validate_isbn10() should return True, but validate_isbn13() should return False. To handle this, I merge the two dictionaries, but then override this ISBN to ensure that it is set to True (it is a valid ISBN for the general purpose method).

While I was building this test method, I learned a new way to merge dictionaries, by using the ** notation. It turns out that PEP 448 (introduced in Python 3.5, I think) introduces a shorthand to “unpack” a dictionary… and that shorthand is super valuable when creating another dictionary. Using this mechanism I was able to merge the two dictionaries, and then “override” the setting for 0136091814 to the correct value in a very straightforward way:

        items_to_test = {**TestISBNValidator.test_data_isbn10,
                         **TestISBNValidator.test_data_isbn13,
                         "0136091814": True}

I learned about this, and also a new feature in Python 3.9 that allows even simpler dictionary merges using the | operator in an article at GeeksforGeeks. I’m currently using 3.8 though, so I decided that the ** notation was straightforward enough for this example and went with it. This is certainly much more readable than the mechanism using update() that I was getting ready to use. It just goes to show that sometimes you learn more writing tests than the code itself! 🙂

During this process, I also discovered that there is a mechanism to convert an ISBN-10 to an ISBN-13 number. I think that will give me something to practice tomorrow!

For completeness, here is the current state of my code.

  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