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.
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.
pip install pyworkforce
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. aNightshift cannot be followed by aMorningshift.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:
# 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:
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:
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_shiftsand skill filtering — shrinks the search space dramatically. - Check the status: always inspect
result["status"]. AnINFEASIBLEresult 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:
- What we control: individual assignments (who works what, when).
- What we want to achieve: sufficient coverage, minimal hours, honored preferences (the objective).
- 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.
Recommended reading
Rodrigo Arenas — ML Engineer & Open Source Maintainer
Working on something similar? I can help you take it to production.