It’s not a hyperbole to say that Python has taken over the pinnacle of Mount Coderest, especially so in data science world if not in others. Let’s also be honest, as much as we all love Python, because of it’s syntactical sugar and the dynamic nature of the language helping in quickly vrooming off from idea to prototype to production and then to analytics and beyond, there are certain pain points (in my opinion) associated with it’s maintenance as the codebase gets larger and larger. Libraries like Pandas and numpy use dtype to circumvent these shortcomings, but the core python itself continues to be sans type-checks.

Because of this, most often than not, a project in Python which starts out as a quick script to test the waters and before you know it, you are debugging a production system with codebase the size of a football field. Those are the long days (and nights) when the dynamic nature of Python, the very feature that earned Python it’s accolades, becomes a nightmarish scenario until you fix those pesky bugs. By then, if you are anything like me, end up having print and stderr statements all over, strewn like landmines of a battlefield.

It’s almost with shame I admit, I can’t believe why I did not pay attention to Python Types and Tests and embrace those as part of daily coding life, until now. As I had this light bulb going off moment for myself, I wanted to share a thing or two of my learnings into Python Type Checking & Doc Tests. So, here we go ….

type-checks

Type-Checking

Let’s explore a Stack (abstract) data type and include type-checks. Later we will add doc-tests to it and run the tests.

#!/usr/bin/env python

#-----------------------------
# @author: Mahesh Vangala
# @license: <MIT + Apache 2.0>
#-----------------------------

"""STACK (Abstract Data Types)

  Goal is to implement 'strict typing' and 'doctests'

  Functionality: (methods) push, pop
"""

from __future__ import annotations
from typing import List, TypeVar, Generic

T = TypeVar("T")

class EmptyStackException(BaseException):
  pass

class Stack(Generic[T]):
  def __init__(self) -> None:
    self.items: list[T] = []
    
  def push(self, item: T) -> T:
    self.items.append(item)
    return item

  def pop(self) -> T:
    if self.is_empty():
      raise EmptyStackException(str(self))
    return self.items.pop()

  def __len__(self) -> int:
    return len(self.items)

  def __bool__(self) -> bool:
    return not self.is_empty()

  def is_empty(self) -> bool:
    return len(self.items) == 0

  def __str__(self) -> str:
    info: str = ""
    if self.is_empty():
      info = "Stack is empty."
    else:
      for idx, val in enumerate(self.items[::-1]):
        info += "#{index}: {val}\n".format(index = idx + 1, val = val)
    return info.strip()

if __name__ == "__main__":
  s: Stack = Stack()
  print(s)
  s.push("Howdy")
  s.push("Hello")
  s.push("Mornin'")
  print(s)
  print(s.pop())
  print(s)
  try:
    while True:
      print(s.pop())
  except BaseException as e:
    print(e)

Use mypy to check for any type mismatches before running the script.

pip install mypy
mypy stack.py
# Try mixing types (ex. int and str) and run again to see mypy in action.

Now, running the script python stack.py will output,

Stack is empty.
#1: Mornin'
#2: Hello
#3: Howdy
Mornin'
#1: Hello
#2: Howdy
Hello
Howdy
Stack is empty.

doc-tests

Doc Tests

Let’s add doc tests …

#!/usr/bin/env python

#-----------------------------
# @author: Mahesh Vangala
# @license: <MIT + Apache 2.0>
#-----------------------------

"""STACK (Abstract Data Types)

  Goal is to implement 'strict typing' and 'doctests'

  Functionality: (methods) push, pop
"""

from __future__ import annotations
from typing import List, TypeVar, Generic

T = TypeVar("T")

class EmptyStackException(BaseException):
  pass

class Stack(Generic[T]):
  def __init__(self) -> None:
    """
    >>> s = Stack()
    >>> print(s)
    Stack is empty.
    """
    self.items: list[T] = []
    
  def push(self, item: T) -> T:
    """
    >>> s = Stack()
    >>> s.push(10)
    10
    >>> s.push(20)
    20
    >>> print(s)
    #1: 20
    #2: 10
    """
    self.items.append(item)
    return item

  def pop(self) -> T:
    """
    >>> s = Stack()
    >>> s.push("Dessert")
    'Dessert'
    >>> s.pop()
    'Dessert'
    >>> s.pop()
    Traceback (most recent call last):
      ...
    stack.EmptyStackException: Stack is empty.
    """
    if self.is_empty():
      raise EmptyStackException(str(self))
    return self.items.pop()

  def __len__(self) -> int:
    """
    >>> s = Stack()
    >>> s.push(1)
    1
    >>> s.push(2)
    2
    >>> len(s) == 2
    True
    >>> s.pop()
    2
    >>> len(s) == 2
    False
    """
    return len(self.items)

  def __bool__(self) -> bool:
    """
    >>> s = Stack()
    >>> bool(s)
    False
    >>> s.push("stuff")
    'stuff'
    >>> bool(s)
    True
    """
    return not self.is_empty()

  def is_empty(self) -> bool:
    """
    >>> s = Stack()
    >>> s.is_empty()
    True
    >>> s.push("stuff")
    'stuff'
    >>> s.is_empty()
    False
    """
    return len(self.items) == 0

  def __str__(self) -> str:
    """
    >>> s = Stack()
    >>> print(s)
    Stack is empty.
    >>> s.push("stuff")
    'stuff'
    >>> print(s)
    #1: stuff
    """
    info: str = ""
    if self.is_empty():
      info = "Stack is empty."
    else:
      for idx, val in enumerate(self.items[::-1]):
        info += "#{index}: {val}\n".format(index = idx + 1, val = val)
    return info.strip()

if __name__ == "__main__":
  s: Stack = Stack()
  print(s)
  s.push("Howdy")
  s.push("Hello")
  s.push("Mornin'")
  print(s)
  print(s.pop())
  print(s)
  try:
    while True:
      print(s.pop())
  except BaseException as e:
    print(e)

Running doc-tests using python -m doctest -v stack.py will run the tests.

#...
7 items passed all tests:
   4 tests in stack.Stack.__bool__
   2 tests in stack.Stack.__init__
   6 tests in stack.Stack.__len__
   4 tests in stack.Stack.__str__
   4 tests in stack.Stack.is_empty
   4 tests in stack.Stack.pop
   4 tests in stack.Stack.push
28 tests in 10 items.
28 passed and 0 failed.
Test passed.


Happy Python Coding! :+1:

Buy Me A Coffee