Library to verify GTINs of various lengths I'm writing a library to verify GTINs (GTIN-8, GTIN-12, GTIN-13, GTIN-14) and calculate their check digits. The calculation of the check digits don't differ from type to type, so I can use the same code for all types of GTINs.

My question: Should I use separate classes for each type of GTIN or should I store the length into an instance attribute?

I have listed two options (simplified):

Option #1

import re

class GTIN:
    def __init__(self, gtin):
        self.gtin = self.parse(gtin)

    @classmethod
    def parse(cls, gtin):
        parsed_gtin = cls.sanitize(gtin)
        if not cls.check_format(parsed_gtin)
            return None
        return parsed_gtin

    @classmethod
    def sanitize(cls, gtin):
        return re.sub(r'[\s\-]+', '', gtin)

    @classmethod
    def check_format(cls, gtin):
        if re.match(cls.FORMAT, gtin):
            return True
        return False

class GTIN8(GTIN):
    FORMAT = r'^[0-9]{7}[0-9?]{1}$'

class GTIN12(GTIN):
    FORMAT = r'^[0-9]{11}[0-9?]{1}$'


The given GTIN string can contain a ? if the check digit should be calculated.

Option #2

import re

class GTIN:
    ALLOWED_LENGTHS = [8, 12, 13, 14]

    def __init__(self, gtin):
        self.gtin = self.parse(gtin)

    @classmethod
    def parse(cls, gtin):
        parsed_gtin = cls.sanitize(gtin)
        if not cls.check_format(parsed_gtin)
            return None
        return parsed_gtin

    @classmethod
    def sanitize(cls, gtin):
        return re.sub(r'[\s\-]+', '', gtin)

    @classmethod
    def check_format(cls, gtin):
        if len(gtin) in cls.ALLOWED_LENGTHS:
            self.length = len(gtin)
        else:
            return False

        format = r'^[0-9]{{{}}}[0-9?]{{1}}$'.format(self.length - 1)
        if re.match(format, gtin):
            return True
        return False
I assume the variety of GTIN's are a bounded set.  If so, I see no use
in the inheritance.  Go with Option #2. And on that option I would recommend a
few changes

Pre-build your formats into a dict based on length:

This pre-builds and compiles the format regex's

formats = {l: re.compile(r'^[0-9]{{{}}}[0-9?]{{1}}$'.format(l - 1))
           for l in (8, 12, 13, 14)}


Add a __str__ method for easier debugging:

This makes it easier to see what is going on in the debugger.

def __str__(self):
    return self.gtin if self.gtin is not None else 'invalid'


If you have an early return, put it in the if, not else:

I would generally code something like this:

if len(gtin) in cls.ALLOWED_LENGTHS:
    self.length = len(gtin)
else:
    return False


as this:

if len(gtin) not in cls.formats:
    return False
....


Define a __len__ method if needed:

I removed the self.length as it was not used internally, and a better way to publish the length is with a __len__.

def __len__(self):
    return len(self.gtin) if self.gtin is not None else 0


Consume bool evaluations directly:

If you need a bool and are performing a test to determine the bool state, you rarely need an if.  I redid this:

if re.match(format, gtin):
    return True
return False


as this (with some other changes also incorporated):

return cls.formats[len(gtin)].match(gtin) is not None


So there is no reason for the if, the result of the test can be returned (or assigned to a variable) directly.

Entire Class:

import re

class GTIN:
    formats = {l: re.compile(r'^[0-9]{{{}}}[0-9?]{{1}}$'.format(l - 1))
               for l in (8, 12, 13, 14)}

    def __init__(self, gtin):
        self.gtin = self.parse(gtin)

    def __str__(self):
        return self.gtin if self.gtin is not None else 'invalid'

    def __len__(self):
        return len(self.gtin) if self.gtin is not None else 0

    @classmethod
    def parse(cls, gtin):
        parsed_gtin = cls.sanitize(gtin)
        if not cls.check_format(parsed_gtin):
            return None
        return parsed_gtin

    @classmethod
    def sanitize(cls, gtin):
        return re.sub(r'[\s\-]+', '', gtin)

    @classmethod
    def check_format(cls, gtin):
        if len(gtin) not in cls.formats:
            return False
        return cls.formats[len(gtin)].match(gtin) is not None