Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Reusing shared geospatial designs and specializing them for new tasks

Open In Colab

In the previous chapters, you learned how to create a class from scratch, define its initial state using __init__(), and attach logic to it using methods. We built standalone spatial entities like a GeoPoint and a BoundingBox.

However, Object Oriented Programming becomes more powerful when you stop thinking only about isolated classes and begin thinking about relationships between classes.

As your geospatial applications grow, writing every class from scratch becomes highly inefficient. Imagine you are modeling a transportation network. You might need a Road class, a River class, and a Railroad class. All three are line features. All three need to calculate their length, store a name, and list their coordinate vertices. If you write three separate classes from scratch, you will type the exact same code three times.

Object Oriented Programming solves this through inheritance. If several classes share the same core structure, rewriting that shared code is wasteful. Inheritance provides a much cleaner solution: you design a “parent” class with generic functionality, and then create specialized “child” classes that automatically absorb the parent’s logic while adding their own unique features.


1. Parent and child classes

Suppose you already have a class that represents a generic geographic point. It stores coordinates, knows its Coordinate Reference System, and can compute distances to other points.

Now imagine that you want to represent a city, a sensor station, and a mountain summit. All of them are fundamentally points. They all need coordinates, and they all benefit from a distance method. However, each of them also needs something more specific—a city needs a population count, while a sensor needs an operational status.

You could copy and paste the same coordinate handling code into every single class. That would work at first. But the moment you improve the point logic, you would need to manually update several separate classes. This is exactly the kind of brittle duplication that Object Oriented Programming helps avoid.

Inheritance is the process by which one class (the child class or derived class) takes on the attributes and methods of another class (the parent class or base class). It is best understood as reuse plus specialization. The parent class captures the common structure, while the child class keeps that structure and adds something more specific.

The syntax for inheritance in Python is remarkably straightforward. You place the parent class inside parentheses after the child class name:

class PointFeature:
    pass


class NamedPoint(PointFeature):
    pass

The line class NamedPoint(PointFeature): tells Python to link these two blueprints.

The core mechanism making this work is a fallback search. If a NamedPoint object is asked to read an attribute or execute a method, Python looks inside the NamedPoint class first. If it cannot find the logic there, it automatically climbs the hierarchy and searches inside PointFeature. This means the child class can safely reuse the parent’s behavior without you having to redefine a single line of code.


2. A geospatial example

Let us look at the syntax for inheritance. To make a child class inherit from a parent class, you simply put the name of the parent class in parentheses when defining the child class.

We begin with a parent class called PointFeature. It represents a generic point in geographic space, stores coordinates, and can calculate distances.

import math


# 1. The Parent Class
class PointFeature:
    def __init__(self, x, y, crs="EPSG:4326"):
        self.x = x
        self.y = y
        self.crs = crs

    def distance_to(self, other):
        return math.hypot(self.x - other.x, self.y - other.y)

Now, let us create a specialized child class called NamedPoint to represent a city or landmark. Notice the (PointFeature) in the class definition. For the moment, we will manually redefine the setup logic in the child class.

# 2. The Child Class (inherits from PointFeature)
class NamedPoint(PointFeature):
    def __init__(self, x, y, name, crs="EPSG:4326"):
        self.x = x
        self.y = y
        self.crs = crs
        self.name = name

While this initialization repeats some code from the parent (a flaw we will fix in the very next section), the true power of inheritance is immediately visible when we test the methods.

zurich = NamedPoint(8.5417, 47.3769, "Zurich")
bern = NamedPoint(7.4474, 46.9480, "Bern")

print(zurich.name)

# We call the method inherited from the parent
dist = zurich.distance_to(bern)
print(f"Distance: {dist:.2f} degrees")
Zurich
Distance: 1.18 degrees

When we call zurich.distance_to(), Python first looks inside the NamedPoint class. When it does not find a distance_to() method there, it climbs the inheritance tree, finds the method in the parent PointFeature class, and executes it.

You can prove this “is a” relationship directly using Python’s built in isinstance() function.

print(isinstance(zurich, NamedPoint))
# Output: True

print(isinstance(zurich, PointFeature))
# Output: True
True
True

Because NamedPoint inherits from PointFeature, the zurich object is recognized as belonging to both categories. The child class is more specific, but it still belongs to the broader parent category.

Interactive Explorer: Method Resolution Order (MRO).
Click the buttons above to visualize how Python searches the inheritance tree to execute your code. Notice how it always checks the specific instance first before falling back to the parent blueprints. For improved visibility of the explorer, follow this link.


3. Using super() to extend initialization

In the previous section, our NamedPoint class repeated the coordinate setup code (self.x = x, self.y = y) that was already defined in PointFeature. That is not ideal. A specialized child class usually needs to initialize its own unique data in addition to the parent’s data, but it should never rewrite the parent’s logic.

To achieve this cleanly, Python provides the built in super() function. super() allows a child class to explicitly call a method from its parent class. Most commonly, it is used inside the child’s __init__() method to ensure the parent initializes the shared attributes before the child sets up its specific ones.

Let us rewrite our NamedPoint class to use super():

class NamedPoint(PointFeature):
    # The child requires four arguments
    def __init__(self, x, y, name, crs="EPSG:4326"):

        # 1. Pass the shared attributes up to the parent to handle
        super().__init__(x, y, crs)

        # 2. Handle the specialized attribute locally
        self.name = name

Now let us test it:

geneva = NamedPoint(6.1432, 46.2044, "Geneva")

# Inherited from parent
print("Coordinates:", geneva.x, geneva.y)
print("CRS:", geneva.crs)

# Specific to child
print("Name:", geneva.name)
Coordinates: 6.1432 46.2044
CRS: EPSG:4326
Name: Geneva

The line super().__init__(x, y, crs) tells Python to run the parent class initialization first. After the parent has securely stored the coordinates and the CRS, the child class takes over to add its own new attribute, name.


4. Overriding methods

Sometimes, a child class needs to perform the exact same action as the parent, but do it differently. This is called overriding a method.

If you define a method in the child class with the exact same name as a method in the parent class, the child’s method will take precedence. Python will find it first and stop looking up the inheritance tree.

Let us define a Geometry parent class that has an abstract way to describe itself. Then we will create a Polygon child class that completely overrides that description to provide more specific spatial details.

class Geometry:
    def __init__(self, crs):
        self.crs = crs

    def describe(self):
        return f"A generic geometry in {self.crs}"


class Polygon(Geometry):
    def __init__(self, crs, area):
        super().__init__(crs)
        self.area = area

    # This method completely overrides the parent's method
    def describe(self):
        return f"A Polygon covering {self.area} sq units in {self.crs}"
base_geom = Geometry("EPSG:4326")
lake_poly = Polygon("EPSG:4326", 500)

print(base_geom.describe())
print(lake_poly.describe())
A generic geometry in EPSG:4326
A Polygon covering 500 sq units in EPSG:4326

Extending instead of replacing

Sometimes a child class should not completely replace the parent’s behavior, but rather extend it. Just as we used super() in __init__(), we can use it to call the parent’s version of any method and then build on top of it.

For example, imagine a SensorStation that inherits from a generic PointFeature. When asked to format its state via the __str__ dunder method, it can grab the parent’s basic coordinate string and simply append the sensor readings to it:

class PointFeature:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Location: ({self.x}, {self.y})"


class SensorStation(PointFeature):
    def __init__(self, x, y, station_id, variable):
        super().__init__(x, y)
        self.station_id = station_id
        self.variable = variable

    # Extending the parent's method
    def __str__(self):
        # 1. Get the baseline string from the parent
        parent_text = super().__str__()
        # 2. Add the child-specific data
        return f"{parent_text} | ID: {self.station_id} | Measuring: {self.variable}"
station = SensorStation(8.55, 47.37, "ZH001", "Temperature")
print(station)
Location: (8.55, 47.37) | ID: ZH001 | Measuring: Temperature

Overriding is a powerful feature because it allows systems to be flexible. If another part of your code expects to call .describe() on any piece of geometry, it does not need to know if the object is a generic geometry, a polygon, or a line. It just calls .describe() and trusts that the specific object will handle the request correctly based on its own overridden logic.


5. Designing useful hierarchies

Now that you know the syntax, the more important issue is architectural design.

Because inheritance is so powerful, it is tempting to use it everywhere to avoid typing. However, the key question is not just “Do these classes share code?” but rather “Does the child class represent a more specific form of the parent class?”

Good inheritance design

Applying this rule to geospatial objects makes the boundaries clear. These are strong candidates for inheritance:

  • An ElevationRaster is a RasterLayer

  • A TemperatureSensor is a Sensor

  • A NamedPoint is a PointFeature

In each case, the child truly is a more specific subtype of the parent. Inheritance fits perfectly here.

Weak inheritance design and Composition

Sometimes two classes interact closely, but one is not a subtype of the other.

For example, a Route is composed of points, and a Map contains raster layers. However:

  • A Route is not a PointFeature

  • A Map is not a RasterLayer

If the relationship is better described by the phrase “has a” (a route has a sequence of points, a map has a raster layer), you should use composition. Instead of inheriting, simply pass the objects as attributes into the new class.

A caution on complexity

Inheritance helps reduce redundant code, but too much inheritance can make designs incredibly difficult to understand. If you create many layers of parent, child, and grandchild classes, the path of attribute lookup becomes impossible to follow.


6. Exercise: Building a Spatial Hierarchy

It is time to practice inheritance directly by building a small system of cooperating classes.

The Scenario: You are managing a network of wildlife tracking collars. Every collar is fundamentally a point in space, but it also has specialized hardware properties like an ID and a battery level.

Your Tasks:

A. The Parent Class (PointFeature)

  1. Define the class. Its __init__ method should accept and store x and y coordinates.

  2. Provide a distance_to(other) method that calculates the Euclidean distance to another point. (Hint: math.hypot(self.x - other.x, self.y - other.y)).

  3. Provide a __str__ method that returns "Location: (x, y)".

B. The Child Class (MobileSensor)

  1. Inherit from PointFeature.

  2. The __init__ method should accept x, y, sensor_id, and battery. Use super() to handle the coordinate initialization.

  3. Add a completely new method called record_movement(dx, dy). It should update the x and y coordinates by the given deltas, and decrease the battery attribute by 1.

  4. Override the __str__ method to extend the parent’s string. Call super().__str__() to get the location text, and append the sensor ID and battery percentage to it.

C. The Simulation

  1. Instantiate two sensors at different coordinates.

  2. Print the distance between them.

  3. Move one of the sensors.

  4. Print both sensors to see the final output.

D. Reflection Look at the code you just wrote and ask yourself:

  • Which methods were inherited without any changes?

  • Which methods were extended using super()?

  • Which methods were entirely unique to the child class?

import math

# Write your code here

7. Summary and Outlook

Inheritance allows one class to build seamlessly on another. A parent class captures the shared spatial structure, and a child class reuses that structure while adding more specific attributes or behavior.

In this chapter, you learned that:

  • Parent and child classes: A parent class defines shared structure and behavior, which a child class inherits automatically.

  • The super() function: This built in function lets child classes reuse parent initialization cleanly, preventing redundant code.

  • Overriding methods: By redefining a parent’s method, a child class can behave more specifically to suit its specialized role.

  • Architectural restraint: Inheritance is most useful when the child is genuinely a more specific kind of the parent. In geospatial programming, a simple and shallow hierarchy is almost always better than an elaborate one.

Outlook: You now understand the core mechanics of Object Oriented Programming: classes, state, methods, and inheritance. But when should you actually use them? In spatial data science, you do not always need to build a complex system of classes. In the next chapter, we will broaden the design picture by looking at Procedural vs. Object Oriented Design, exploring when to use simple standalone functions and when to commit to a full class based architecture.