You can achieve code reuse and a more organized structure in your Locust tests by inheriting from existing User classes and composing behaviors.

Here’s a simple Locust file demonstrating inheritance and composition:

from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)

    @task
    def index(self):
        self.client.get("/")

class ProductPageUser(WebsiteUser):
    @task
    def view_product(self):
        self.client.get("/products/123")

class CartUser(ProductPageUser):
    @task
    def add_to_cart(self):
        self.client.post("/cart/add", json={"product_id": 123, "quantity": 1})

    @task(10) # This task will run 10 times more often than others
    def view_cart(self):
        self.client.get("/cart")

This example shows how ProductPageUser inherits from WebsiteUser, gaining the index task. CartUser then inherits from ProductPageUser, inheriting both index and view_product tasks, and adds its own add_to_cart and view_cart tasks.

The fundamental problem this solves is managing complexity as your load tests grow. Without inheritance and composition, you’d end up with a lot of duplicated code, making it hard to update common test logic or create variations of user behaviors. For instance, if you needed to add a common header to all requests, you could add it to WebsiteUser, and all its subclasses would automatically get it.

Internally, Locust processes these classes by creating a single, combined set of tasks for each User class at runtime. When CartUser is instantiated, Locust effectively merges the tasks defined in CartUser, ProductPageUser, and WebsiteUser into a single pool of executable tasks for that user type. The @task decorator assigns weights, and Locust’s scheduler picks tasks based on these weights and the wait_time defined.

The key levers you control are:

  • Inheritance: Use class MyUser(BaseUser): to inherit tasks and attributes like wait_time and host.
  • Task Decorator (@task): Assign tasks to a user class. The optional integer argument is a weight, influencing how often a task is chosen relative to others within the same class.
  • Composition: A user class can have tasks defined directly, and also inherit tasks from its parent classes. This allows you to build complex behaviors by combining simpler ones.
  • HttpUser vs. User: HttpUser provides a self.client (an instance of HttpSession) pre-configured for HTTP requests, including automatic handling of cookies and response status codes. User is more generic.

Most people don’t realize that when you override a task from a parent class, you can still call the parent’s task using super(). For example, if ProductPageUser wanted to also hit the index page before viewing a product, it could do this:

class ProductPageUser(WebsiteUser):
    @task
    def view_product(self):
        super().index() # Call the index task from the parent
        self.client.get("/products/123")

This allows for extending parent behavior rather than just replacing it, offering a powerful way to build upon existing test logic.

The next concept you’ll likely want to explore is how to dynamically generate tasks or user classes based on external data.

Want structured learning?

Take the full Locust course →