object oriented – The Liskov Substitution Principle, and Python

Background

I’ve taught myself Python over the past year-and-a-bit, and would consider myself an intermediate Python user at this point, but never studied computing at school/university. As such, my knowledge of theory is a little weak. Python is the only language I know.

I’m trying to wrap my head around the Liskov Substitution Principle so that I can write better, more object-oriented code.

Question 1: The Python data model

As we know, in Python 3, all custom classes implicitly inherit from object. But the docstring of object includes this line:

When called, it accepts no arguments and returns a new featureless instance that has no instance attributes and cannot be given any.

With this in mind, how can any python classes that accept a nonzero number of parameters for their constructors be said to comply with the LSP? If I have a class Foo, like so:

class Foo:
    def __init__(self, bar):
        self.bar = bar

then, surely this class definition (and all others like it) violates the contract specified by object? object and Foo cannot be used interchangeably, as object accepts exactly 0 parameters to its constructor, while Foo accepts exactly 1.

Question 2: collections.Counter

As Raymond Hettinger tells us, the Python dictionary is an excellent example of the open/closed principle. The dict class is “open for extension, closed for modification” — if you wish to modify some of the prepackaged behaviours of a dict, you’re advised to inherit from collections.abc.MutableMapping or collections.UserDict instead.

collections.Counter, however, is a direct subclass of dict. While it is mostly an extension of dict rather than a modification of behaviours already defined in dict, this isn’t true for collection.Counter.fromkeys. With a standard dict, this classmethod is an alternative constructor for a dictionary, but the method is overridden in Counter so that it raises an exception. Here is the comment explaining why this is the case:

# There is no equivalent method for counters because the semantics
# would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2).
# Initializing counters to zero values isn't necessary because zero
# is already the default value for counter lookups.  Initializing
# to one is easily accomplished with Counter(set(iterable)).  For
# more exotic cases, create a dictionary first using a dictionary
# comprehension or dict.fromkeys().

The explanation makes sense — but surely this violates the LSP? According to the Wikipedia article, the LSP states that “New exceptions cannot be thrown by the methods in the subtype, except if they are subtypes of exceptions thrown by the methods of the supertype.” dict.fromkeys does not throw a NotImplementedError; collections.Counter does.

Comments

I’m interested in whether these examples do, in fact, break the LSP. However, I’m also interested in why they break the LSP, if indeed they do. In what situations is enforcing the LSP necessary/advisable/useful? In what situations is worrying about the LSP more of a meaningless distraction? Etc.