Django / FastAPI / Celery / Postgres

Hire Senior Python
Developers in Morocco

Most engineering is reading, not typing. We hire for the ability to open a codebase somebody else wrote five years ago and ship something safe in week two.

Your repo, your standups, your review process. Morocco on UTC+0, so your engineer is online when your team is.

Meet a Python Engineer

You will talk to an engineer, not an SDR

01 / Diagnosis

What we find when we open your codebase

Every Python product that has shipped for a few years carries the same handful of injuries. None of this is an insult. It is what happens when a team is busy and the product is working.

Recognise three of these and we already know roughly how your first month goes.

01

The N+1 nobody sees

A serializer or template loops a queryset and hits the database once per row. Fine at 40 records. Unusable at 4,000.

What we do

select_related and prefetch_related, then an assertNumQueries in the test so it cannot come back.

02

Business logic living in the Django admin

Someone needed a button. The button became a workflow. Now a core business rule only runs if a human clicks it inside an interface built for data entry.

What we do

Move the rule into a service function the admin calls. Then the API and the scheduled job can call it too.

03

settings.py with a dozen environment branches

if DEBUG, if STAGING, if that one client. Nobody can say what production actually loads without running it.

What we do

One settings module, environment driven, with every difference explicit and typed.

04

Celery tasks that are not idempotent

A retry runs the charge twice. A redelivered message sends the email again. Silent until it is a support ticket about money.

What we do

Idempotency keys, at-least-once assumed rather than hoped against, and a dead letter queue a human actually reads.

05

requests called with no timeout

The default is to wait forever. One slow upstream and the worker pool is gone. This is the most common way a healthy Python service dies for reasons unrelated to its own code.

What we do

timeout= everywhere, retries with backoff, and a circuit breaker on anything you do not control.

06

A bare except swallowing the failure

except: pass wrapped around the flaky part. The bug is still there. It just stopped telling anyone.

What we do

Catch the exception you expect, let the rest surface, and make sure the surface reaches someone on call.

07

Migrations nobody will run

One migration locked a big table during business hours, or a squash went wrong. Now the team edits schema by hand and the migration history is fiction.

What we do

Split the dangerous ones, build indexes with CONCURRENTLY, and make the migration path trustworthy again before anything else ships.

08

No connection pooler in front of Postgres

Every worker opens its own connections. Traffic doubles and the database runs out of connections long before the application runs out of capacity.

What we do

PgBouncer in transaction mode, with the application configured to expect it.

09

Mutable default arguments

def add(item, basket=[]). That list is created once at import and shared by every call for the life of the process. It presents as a haunting.

What we do

None as the default. A two-character fix, and it is still in production somewhere near you.

10

A test suite that asserts nothing

Coverage reports 74%. The tests call the code and check that it did not raise. That is not a safety net, it is a rumour of one.

What we do

Fix the tests before touching the code they are meant to protect. Usually the honest first fortnight.

Finding 01, in full

views.py: before and after
# Before: one query for the list, then one more per row.
# 400 orders on the page means 401 round trips to Postgres.
for order in Order.objects.all():
    print(order.customer.name)

# After: two queries, whatever the row count.
for order in Order.objects.select_related("customer"):
    print(order.customer.name)

# And the part most people skip, so it cannot come back:
with self.assertNumQueries(2):
    render_order_list()

The fix is two lines. The test is why it is still fixed a year from now.

02 / Method

How we work in code we did not write

The fastest way to lose a client is to arrive and start refactoring. The order does not change.

  1. Week 1

    Read, and ship something small

    A real ticket, deliberately contained. It proves the environment works, it proves the review process works, and it gives your team something to judge before anything important is at stake.

  2. Week 2

    Find out what the tests actually cover

    Not the coverage number. Whether the suite would catch a real regression. If it would not, that is the honest first piece of work and we say so rather than build on sand.

  3. Week 3

    Fix what is costing you now

    The slow endpoint, the queue that backs up, the report that times out. Something your team feels, so the value of the engagement stops being theoretical.

  4. Week 4 on

    Earn the right to change structure

    Only after shipping safely several times do we propose anything architectural. Opinions about your architecture are cheap in week one and expensive to act on.

03 / Proof / Holloway Group

Thirty minutes a document,
down to two

~30 min

Per purchase order, before

≤2 min

Per purchase order, after

≥95%

Handled with no human

≥99%

Field-level accuracy

Purchase orders arrived as PDFs in an inbox, in whatever format each supplier felt like using. Staff opened every one, cross-referenced thousands of SKUs by hand, and retyped it into Unleashed. Senior people were spending half their day as a bridge between an email client and an ERP.

The hard part was never reading the PDF
Extraction is the easy half. The real problem is that nobody writes the correct product ID. A supplier types "blue widget 12pk" when the system needs SKU-7823-BL-12. Every template-based automation dies exactly there.
So we stopped matching on strings
The full customer and product database syncs nightly into a vector store. Incoming line items are matched by semantic similarity against real records, so human phrasing resolves to the right SKU with nobody maintaining a lookup table.
Confidence decides who handles it
High-confidence matches post straight through. Low-confidence line items get flagged individually in a review dashboard for a one-click fix. The team audits in seconds instead of re-entering for minutes, and accuracy is not traded for speed.
Built to survive a bad day
Supplier codes validated against the live ERP before anything is written. Retry logic and full error handling on the integration. Every transaction logged with 90-day retention, plus a daily report. Grafana and Prometheus for live visibility.
Zero template maintenance
A new supplier with a new layout needs no configuration at all. That was the design goal, because the alternative is a system that quietly rots the moment nobody is paid to maintain it.
The matching problem, concretely
supplier wrote:  "blue widget 12pk"
ERP expects:     SKU-7823-BL-12

# string match  -> no result, human takes over
# regex/template -> breaks on the next supplier

# what we did: embed both, compare meaning
match = vector_store.search(line_item, top_k=1)

if match.score >= THRESHOLD:
    post_to_erp(match.sku)      # ~95% of lines
else:
    flag_for_review(line_item)  # one click, seconds

The confidence threshold is the whole design. It is what lets the system be fast without being wrong.

Read the full Holloway Group build, or the Talearnted Tutors matching engine.

04 / Screening

What we mean by senior

Everyone writes senior. Three tests, and you can run all three yourself on the call.

01

They have operated Python, not only shipped it

Ask what woke them up, what the cause turned out to be, and what they changed so it did not happen twice. An engineer who has only written features answers this vaguely.

02

They can work in a codebase they did not write

Ask how they would spend week one in a 200,000 line Django project. If the answer is refactoring, that is your answer.

03

They can name a decision that turned out wrong

What it cost, and what they do differently now. An engineer who cannot has either not made enough decisions or is not being straight with you.

Ask to speak to the engineer who would work with you. We put them on the call.

05 / Rates

Rates, and who this is wrong for

$4,800–$7,200

per month / one senior engineer / full time

One number, no separate management fee, no recruitment charge, no markup on hours. If they are ill, on holiday or they resign, that is our cost to carry, not a gap in your sprint.

The rate moves on seniority, how much technical leadership you need, contract length and on-call cover. If you need more than one person, a dedicated development team in Morocco is priced per seat off the same number.

Do not hire us if

You need ten developers next week

We are a small company. We would have to recruit, and we will give you a real date rather than a comfortable one.

Do not hire us if

You have a two week job

A marketplace is genuinely the right tool for that, and it will cost you less.

Do not hire us if

You want the cheapest hourly rate

We are not it, and we will not pretend to be.

Do not hire us if

You want us to disappear for three months

If you want to hand over a spec and hear nothing until delivery, we are a bad fit on purpose.

06 / Questions

Straight answers

What if my codebase is a mess?

Most are. That is the work, and it is what we screen for when we hire. The list at the top of this page is not hypothetical.

Can they work on an existing project or only new builds?

Existing, mostly. A greenfield Python project is the rare engagement. Reading someone else's Django is a specific skill and it is the one we hire for.

Which parts of the Python stack?

Django and FastAPI, Celery for background work, Postgres, Docker, and the AWS or Hetzner infrastructure underneath. If you need deep machine learning, say so early, because that is not our strength and Morocco is thin there generally.

Can I interview them first?

Yes. We do not place anyone you have not met and approved.

How big is your team?

Small, deliberately. Every engagement has senior technical oversight and you work with the people you meet. Anyone new is introduced before they start.

Why Morocco rather than Eastern Europe or LATAM?

Timezone and language. Morocco has been on UTC+0 all year since September 2026: most of a working day with Europe and four to five hours with New York, in English and French. On cost, Lemon.io puts Morocco senior rates at $30 to $41 an hour in its April 2026 report, the lowest band it tracks.

Do you work with early stage startups?

Yes, if there is a real product and a real budget. We are a poor fit for pre-funding equity work.

Who owns the code?

You do, from the first commit. IP is assigned in the contract, not on final payment, and NDAs are signed before anyone opens your repository.

Bring us the slow endpoint

Thirty minutes. Bring the actual problem, not a tidied version of it. You will get a real opinion whether or not you hire us.

Meet a Python Engineer

Also for you

AWS engineers who have been on call / a dedicated development team in Morocco