Operations Research
Python
pyworkforce
scheduling
rostering
OR-Tools
optimization

How to Solve Scheduling Problems in Python

Solve employee scheduling and rostering problems in Python with pyworkforce. Assign individual workers to shifts while honoring coverage, rest rules, banned shifts, and preferences.

May 14, 2026·6 min read

In the previous article on workforce planning, we saw how to determine how many workers you need. Now we tackle a distinct and more concrete problem: given a set of available employees, who do you assign to which shift on which day? This is the scheduling or rostering problem, and it adds one extra ingredient: assignments are individual rather than aggregate.

The conceptual difference matters. Workforce planning works with aggregate numbers (how many people per day). Rostering works with individual assignments (does Alice work the morning shift on Tuesday?). This extra granularity makes it richer but also more complex — and it's exactly what pyworkforce's rostering module is built for.

What Is Scheduling Optimization?

Employee scheduling is the process of assigning specific workers to specific shifts while satisfying a set of constraints. In its simplest form, each shift on each day has a staffing requirement and each worker can either be assigned to it or not.

Typical constraints include:

  • Minimum coverage: each shift on each day must have at least N people.
  • Maximum/minimum hours: a worker must work within an allowed range of hours.
  • Rest rules: a worker needs a minimum amount of rest, and certain shift sequences (a night shift followed by a morning shift) should be forbidden.
  • Availability: some workers cannot work certain days or shifts.
  • Preferences: where possible, honor the shifts workers prefer.

The objective function can minimize the gap between required and assigned staff, minimize total hours or cost, or maximize preference satisfaction.

The Rostering Problem

Under the hood, rostering is a mixed-integer program with binary decision variables: x[w][s][d] = 1 if worker w is assigned to shift s on day d, and 0 otherwise. Coverage requirements, hour limits, banned shifts, and rest rules all become linear constraints over those variables, and the objective minimizes total assigned hours (or cost).

Writing all of that by hand is tedious and error-prone — every constraint has to be encoded variable by variable. pyworkforce builds and solves that model for you from a high-level description, using Google OR-Tools' CP-SAT solver underneath.

pyworkforce Rostering: MinHoursRoster

The MinHoursRoster solver assigns named resources to shifts across a horizon of days, minimizing total working hours while respecting every constraint you declare.

bash
pip install pyworkforce
python
from pyworkforce.rostering.binary_programming import MinHoursRoster

resources = ["Alice", "Bob", "Carlos", "Diana", "Eve"]

solver = MinHoursRoster(
    num_days=7,
    resources=resources,
    shifts=["Morning", "Afternoon", "Night"],
    shifts_hours=[8, 8, 8],
    min_working_hours=40,
    max_resting=2,
    non_sequential_shifts=[{"origin": "Night", "destination": "Morning"}],
    banned_shifts=[
        {"resource": "Alice", "shift": "Night", "day": 5},
    ],
    required_resources={
        "Morning":   [2, 2, 2, 2, 2, 1, 1],
        "Afternoon": [2, 2, 2, 2, 2, 1, 1],
        "Night":     [1, 1, 1, 1, 1, 1, 1],
    },
    resources_preferences=[
        {"resource": "Bob", "shift": "Morning"},
    ],
)

result = solver.solve()
print(f"Status: {result['status']}, Cost: {result['cost']}")
print(f"Shifts assigned: {result['total_shifts']}")

Each argument maps to a real-world rule:

  • shifts_hours — the length of each shift, so the solver can enforce the working-hours range.
  • min_working_hours — the minimum hours each resource must be scheduled for over the horizon.
  • max_resting — the maximum number of rest days a resource may have.
  • required_resources — for each shift, how many people are needed on each day (the output of the planning/scheduling stage).
  • non_sequential_shifts — forbidden shift transitions, e.g. a Night shift cannot be followed by a Morning shift.
  • banned_shifts — hard unavailability for a specific resource, shift, and day.
  • resources_preferences — soft preferences the solver tries to honor.

Reading the Schedule from the Solution

The solution dictionary contains the per-person assignments and rest days, so all the constraint bookkeeping you'd otherwise do by hand is already done:

python
# Individual assignments: who works which shift on which day
for assignment in result["resource_shifts"]:
    if assignment["shift"] != "False":  # "False" marks a rest slot
        print(
            f"Day {assignment['day']:>1} | "
            f"{assignment['resource']:<8} -> {assignment['shift']}"
        )

# Rest days per resource
print("\nRest days:")
for rest in result["resting_resource"]:
    print(f"  {rest['resource']}: day {rest['day']}")

Because pyworkforce returns the full assignment plus each person's rest days, building a readable weekly calendar or a per-employee workload summary is just a matter of grouping this list — no need to decode raw solver variables.

Modeling Real-World Constraints

The strength of the rostering model is how naturally business rules map onto its parameters.

Rest Rules and Forbidden Sequences

Minimum rest is handled by max_resting (how many days off are allowed) together with min_working_hours (the floor on hours worked). Forbidden transitions — the classic "no morning shift right after a night shift" — go in non_sequential_shifts:

python
non_sequential_shifts=[
    {"origin": "Night", "destination": "Morning"},
    {"origin": "Night", "destination": "Afternoon"},
]

Availability and Preferences

Hard unavailability (vacation, training, legal limits) is expressed as banned_shifts, which the solver treats as inviolable. Softer wishes go in resources_preferences, which the solver honors when it can without breaking a hard constraint:

python
banned_shifts=[
    {"resource": "Diana", "shift": "Morning", "day": 0},  # Diana is off Monday morning
]
resources_preferences=[
    {"resource": "Bob", "shift": "Morning"},   # Bob prefers mornings
    {"resource": "Eve", "shift": "Night"},     # Eve prefers nights
]

Multi-Skill Teams

When workers cover different roles, pyworkforce provides a dedicated pyworkforce.staffing module for multi-skilled staff-mix optimization, so you can model who is qualified for which role separately from who is assigned to which shift — rather than hand-encoding role-specific variables.

Scalability Considerations

Rostering with binary variables is a mixed-integer program, which is NP-hard in general. pyworkforce delegates the heavy lifting to OR-Tools' CP-SAT solver, a state-of-the-art engine that handles realistic instances (dozens of employees, multiple shifts, a weekly horizon) efficiently. For very large problems:

  • Decompose the horizon: when days are independent, solve week by week (a rolling horizon).
  • Tighten the model: fewer eligible (resource, shift) combinations — via banned_shifts and skill filtering — shrinks the search space dramatically.
  • Check the status: always inspect result["status"]. An INFEASIBLE result means the constraints contradict each other (for example, demanding more coverage than the available resources can supply).

Conclusion

Employee scheduling is a combinatorial optimization problem that maps naturally to constraint programming. pyworkforce lets you model it declaratively — describing resources, shifts, coverage, and rules — without sacrificing the power of the underlying OR-Tools solver.

The key to formulating these problems correctly is still distinguishing between:

  1. What we control: individual assignments (who works what, when).
  2. What we want to achieve: sufficient coverage, minimal hours, honored preferences (the objective).
  3. What cannot be violated: rest rules, availability, capacity (the hard constraints).

With that structure clear, pyworkforce handles the rest. The solver finds in seconds assignments that would take hours of manual coordination, and the solution is optimal within the model as stated.


About the author

Rodrigo Arenas is a software architect and machine learning engineer in Medellín, Colombia. He builds products, platforms, and open-source software for AI, Data Engineering, and Machine Learning — creator of Ciaren, sklearn-genetic-opt, and PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

Working on something similar? I can help you take it to production.

Building an AI or data platform?

Products, platforms, and open-source software for AI, Data Engineering, and Machine Learning.

View Projects
Vision

Building an ecosystem of products and open-source software for AI, Data Engineering, and Machine Learning.

Projects
sklearn-genetic-optPyWorkforceCiaren

Iterixs (Currently building)

© 2026 Rodrigo Arenas