dj-ratelimit-middleware
Flexible rate limiting for Django, Django REST Framework, and ASGI.
dj-ratelimit-middleware provides Redis-backed atomic rate limiting or portable Django Cache limits, HTTP policies, authenticated-user and IP identities, and optional WebSocket enforcement. Choose exact sliding windows or constant-memory token buckets according to your workload.
Features
- Two native Redis algorithms: exact sliding-window log and constant-space token bucket, each enforced by atomic Lua operations.
- Alternative Django Cache backend: uses Django's configured cache alias, without requiring the optional
redisPython dependency. - Configurable identities: client IP (default), authenticated user ID (with anonymous-IP fallback), or a trusted custom principal.
- HTTP policies: path, path prefix, methods, priority, algorithm, and up to eight quotas per matching policy.
- Grouped Redis admission: all-or-nothing admission for multiple quotas in one Redis operation on supported standalone servers.
- Framework integration: synchronous/asynchronous Django middleware, an optional DRF throttle, and standalone ASGI HTTP/WebSocket middleware.
- Security-oriented configuration: trusted-proxy networks, bounded forwarded headers, explicit exemptions, strict Django checks, and blocking startup validation.
- Operational controls: configurable fail-open/fail-closed behavior, 429 responses,
Retry-After, and optional rate-limit headers.
Contents
- Requirements and installation
- Quick start
- Storage backends and algorithms
- Identity and authentication
- Endpoint policies
- Django REST Framework
- ASGI and WebSockets
- Responses and headers
- Configuration reference
- Security and deployment considerations
- Performance and testing
- Development
Requirements and installation
- Python 3.10–3.14.
- Django 5.2 on Python 3.10–3.14, or Django 6.0/6.1 on Python 3.12–3.14.
- Optional Django REST Framework 3.18.3 or newer, using the
drfextra. - Optional
redis-py >=5.0, using theredisextra for native Redis storage. Native Redis requires a reachable compatible Redis server.
# Django Cache only (no redis-py required)
pip install dj-ratelimit-middleware
# Native Redis engine
pip install 'dj-ratelimit-middleware[redis]'
# Native Redis plus DRF throttling
pip install 'dj-ratelimit-middleware[redis,drf]'
With uv:
uv add dj-ratelimit-middleware
uv add 'dj-ratelimit-middleware[redis,drf]'
The PyPI distribution name is dj-ratelimit-middleware; the Python import and Django app name is ratelimit_middleware. DRF is optional. The package defines no database models or URL routes.
Quick start
Choose one storage configuration. The examples below use the normal Django middleware entry point.
Option A — Django Cache
Django Cache is an accessible starting point where approximate counter-based limits are sufficient. This minimal example uses Django's built-in, process-local memory cache, so it is intended for development only. In a multi-worker deployment, configure a shared backend such as Redis or Memcached and install whichever client driver that backend requires.
# settings.py
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
"ratelimit": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
}
INSTALLED_APPS = [
# ...
"ratelimit_middleware", # Enables Django system checks
]
MIDDLEWARE = [
# ... other middleware
"ratelimit_middleware.middleware.RateLimitMiddleware",
]
RATELIMIT_STORAGE = {"BACKEND": "cache", "CACHE_ALIAS": "ratelimit"}
RATELIMIT_RATE = 60
RATELIMIT_WINDOW = 60 # seconds
This uses the standard Django Cache API, not the native Redis Lua engine. To deploy a shared cache instead, change CACHES["ratelimit"] to a supported Redis- or Memcached-backed Django cache and install its backend-specific Python driver. The package itself does not require the redis extra for generic cache mode, but Django's Redis cache backend still requires redis-py. See the storage comparison.
Option B — Native Redis (stronger admission guarantees)
Use the redis extra and supply a connection URL explicitly:
# settings.py
import os
INSTALLED_APPS = [
# ...
"ratelimit_middleware",
]
MIDDLEWARE = [
# ...
"ratelimit_middleware.middleware.RateLimitMiddleware",
]
RATELIMIT_STORAGE = {
"BACKEND": "redis",
"URL": os.environ["RATELIMIT_REDIS_URL"],
}
RATELIMIT_RATE = 60
RATELIMIT_WINDOW = 60
RATELIMIT_FAIL_OPEN = False # Consider for critical protected endpoints
The default algorithm is an exact sliding window. To enable the constant-space token bucket instead:
RATELIMIT_ALGORITHM = "token_bucket"
An alternative native Redis configuration can reference a supported simple Django Redis cache alias:
RATELIMIT_STORAGE = {"BACKEND": "redis", "CACHE_ALIAS": "ratelimit"}
For this alias form, the cache backend must be Django's built-in RedisCache or a supported simple django-redis configuration with a single valid URL. Advanced options (e.g., Sentinel, multi-location configurations, custom client options) are not automatically translated into the native Redis client; use an explicit URL when appropriate. The native adapter creates its own client/pool, rather than borrowing the cache backend's private connection internals.
The shorthand RATELIMIT_REDIS_URL = "redis://..." also selects native Redis if RATELIMIT_STORAGE is omitted. Omitting both a valid Redis configuration and an explicit cache backend is an error; a native Redis server URL is not assumed.
Validate the configuration
python manage.py check
Unknown RATELIMIT_* names, invalid algorithm/identity/backend options, nonpositive rate/window values, malformed policies, and unsupported combinations produce blocking check errors and fail middleware initialization. The ratelimit_middleware app must be installed for Django's registered checks; initialization validates settings independently.
Storage backends and algorithms
| Capability | Native Redis | Django Cache |
|---|---|---|
| Algorithm | Exact sliding window or token bucket | First-request-anchored counter window |
| Concurrency | Atomic server-side Lua admission | Depends on underlying cache; no portable atomic admission guarantee |
| Multiple quotas | Atomic group on supported standalone Redis | Sequential; an earlier quota can be consumed before a later denial |
| Memory per identity | Sliding window: O(admitted events); token bucket: O(1) | O(1) counter per quota |
| Reset estimate | Calculated from Redis state | Conservative whole-window estimate |
| Async | Native async Redis client | Django's async cache API |
| Eviction | Redis state loss resets quota | Cache eviction can reset quota |
Sliding window
RATELIMIT_RATE = 60 and RATELIMIT_WINDOW = 60 mean at most 60 admitted requests in any rolling 60-second interval, per rate-limit key on native Redis. Only successful admissions add an event; rejected requests do not grow the event log or refresh its TTL.
Use this when exact rolling-window enforcement matters. Memory scales with the number of admitted events still in the window.
Token bucket
RATELIMIT_ALGORITHM = "token_bucket"
RATELIMIT_RATE = 120
RATELIMIT_WINDOW = 60
The bucket starts full with capacity 120, refills at 120 tokens per 60 seconds, and spends one token per admitted request. This supports bursts up to current capacity and uses constant-size Redis state per identity. It is not equivalent to a strict rolling-window maximum.
Token bucket requires RATELIMIT_STORAGE to use native Redis (or an equivalent valid native Redis URL configuration). The generic Django Cache adapter rejects this algorithm at startup.
For grouped limits, a policy selects one algorithm for its entire group. Mixed-algorithm groups are not supported. Different policies can select different algorithms.
Identity and authentication
IP identity (default)
RATELIMIT_IDENTITY = "ip"
Requests share a quota by resolved client IP. The package normalizes IPv4/IPv6 and can use X-Forwarded-For only when the immediate network peer belongs to explicitly trusted proxies. See trusted proxies.
Authenticated Django users
RATELIMIT_IDENTITY = "user"
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"ratelimit_middleware.middleware.RateLimitMiddleware",
# ... other middleware
]
An already authenticated user's stable primary key is used as the identity; anonymous requests fall back to IP. Authenticated identities are namespaced and SHA-256 hashed before being used in rate-limit keys. Resolving Django's lazy session principal can involve the usual session/authentication lookup; IP mode avoids it.
DRF token/JWT authentication happens later than Django middleware. For those requests, use the optional DRF throttle if you need quotas on the verified DRF user identity. The library does not decode JWTs or authenticate credentials itself.
Trusted custom identities
Custom resolvers are supported for identities the application has already authenticated or verified (e.g., an API-key principal or tenant). Define a synchronous, nonblocking callable:
# myproject/ratelimit.py
def resolve_principal(context):
principal = (
context.get("verified_principal")
if isinstance(context, dict) # ASGI scope
else getattr(context, "verified_principal", None) # Django/DRF request
)
if principal is None:
return None # Use the resolved client IP instead
return ("service", principal.pk)
# settings.py
RATELIMIT_IDENTITY = "custom"
RATELIMIT_IDENTITY_RESOLVER = "myproject.ratelimit.resolve_principal"
The callable receives the framework's request object or ASGI scope. It must return None or a (namespace, identifier) tuple using bounded, supported values. Never use an arbitrary X-User-ID, bearer token, or unverified JWT claim as a principal. Do not perform database or network I/O in this resolver.
Endpoint policies
RATELIMIT_POLICIES is an ordered list of policy dictionaries. The first matching policy after priority sorting replaces the global/default quota for that HTTP request. If nothing matches, the global rate/identity/algorithm applies. Exemptions bypass admission before policy selection.
RATELIMIT_POLICIES = [
{
"NAME": "login",
"PATH": "/api/auth/login/",
"METHODS": ["POST"],
"PRIORITY": 100,
"ALGORITHM": "sliding_window",
"LIMITS": [
{"IDENTITY": "ip", "RATE": 10, "WINDOW": 60},
],
},
{
"NAME": "management",
"PATH_PREFIX": "/api/management/",
"METHODS": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"PRIORITY": 10,
"ALGORITHM": "token_bucket", # Native Redis only
"LIMITS": [
{"IDENTITY": "user", "RATE": 120, "WINDOW": 60},
{"IDENTITY": "ip", "RATE": 600, "WINDOW": 60},
],
},
]
A policy has the following fields:
| Field | Required | Description |
|---|---|---|
NAME |
Yes | Unique lowercase identifier, at most 32 characters |
PATH / PATH_PREFIX |
Exactly one | Literal exact path or path-segment-aware subtree prefix |
METHODS |
No | List/tuple of uppercase HTTP methods; omitted matches any method |
PRIORITY |
No | Integer from -1000 to 1000; higher value wins; declaration order breaks ties |
ALGORITHM |
No | sliding_window (default) or token_bucket (native Redis only) |
LIMITS |
Yes | 1–8 quotas, each containing exactly IDENTITY, RATE, and WINDOW |
A policy may include IP and authenticated-user quotas simultaneously. With native Redis on a supported standalone server, the group is checked atomically: if one quota denies, no quota in that group is charged. With Django Cache, checks are sequential and can partially consume earlier quotas before a denial. Policy counts and integer values are bounded at configuration time (at most 64 policies).
PATH_PREFIX = "/api/manage/" matches /api/manage and its subtree, but not /api/management. HTTP policies currently apply to HTTP, not WebSocket message limits.
Django REST Framework
Install the drf extra and add the throttle to DRF's configuration or a specific view:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"ratelimit_middleware.drf.RateLimitThrottle",
],
}
This adapter runs after DRF authentication and uses a verified user identity by default, falling back to IP for anonymous requests. Its configuration follows the global rate/window and HTTP policies, but the throttle uses separate drf quota keys from the Django middleware's http keys.
If both adapters are active, both enforce their own quotas; this can be desirable for defense in depth, but may also lead to redundant checks and additional backend work. DRF throttles use DRF's 429 and wait/Retry-After handling, whereas the Django middleware produces its own JSON 429 and optional X-RateLimit-* headers. Django middleware exemptions are not a separate automatic exemption mechanism for DRF throttles.
Subclass RateLimitThrottle to change its identity_mode to "ip" or "custom"; custom mode uses identity_resolver or the configured dotted-path resolver. The DRF package is imported only when this integration is used.
ASGI and WebSockets
For a standalone ASGI entry point:
# asgi.py
from django.core.asgi import get_asgi_application
from ratelimit_middleware.middleware import ASGIRateLimitMiddleware
application = ASGIRateLimitMiddleware(get_asgi_application())
This example wraps Django's ASGI application and rate-limits at the outer boundary. If Django's ordinary RateLimitMiddleware is already installed, requests may be limited twice. Choose an entry point intentionally. For authenticated scope["user"] identities (e.g., when using Channels), place the ASGI rate limiter inside trusted authentication middleware so the verified principal already exists in the scope.
The standalone adapter supports:
- HTTP admission, policies, 429 JSON responses, and optional rate-limit headers.
- WebSocket handshake limits when
RATELIMIT_WS_HANDSHAKE = True(default): uses the global rate/window and a dedicated bucket. - WebSocket inbound-message limits when
RATELIMIT_WS_MSG_RATE > 0(disabled by default): usesRATELIMIT_WS_MSG_WINDOW. - WebSocket rejection via
websocket.closewith application-defined code4408.
WebSocket handshake and message quotas are separate from HTTP endpoint policies. Neither Channels nor an ASGI server is a mandatory package dependency.
Responses and headers
When the Django/ASGI middleware rejects a request, it returns HTTP 429 with a JSON body like:
{
"error": "Rate limit exceeded",
"detail": "Please try again in 12 seconds"
}
The retry time varies by algorithm and current state; the example is illustrative. The middleware sets Retry-After and, by default, rate metadata:
| Header | Meaning |
|---|---|
Retry-After |
Estimated seconds before the limiting quota permits another request; on 429 |
X-RateLimit-Limit |
Rate of the selected quota |
X-RateLimit-Remaining |
Estimated remaining capacity for that quota |
X-RateLimit-Reset |
Estimated seconds until quota availability/reset; not an epoch timestamp |
For multiple quotas, headers describe a selected limiting/most-restrictive quota, not every configured quota. Cache-based values are approximate. Header emission can be disabled with RATELIMIT_ENABLE_HEADERS = False; Retry-After on middleware-generated 429 responses remains present.
Configuration reference
All settings are configured as top-level variables in Django's settings.py.
| Setting | Default | Purpose / constraints |
|---|---|---|
RATELIMIT_RATE |
60 |
Positive integer global capacity/request limit |
RATELIMIT_WINDOW |
60 |
Positive integer seconds for global quota |
RATELIMIT_ALGORITHM |
"sliding_window" |
Native Redis "sliding_window" or "token_bucket" |
RATELIMIT_STORAGE |
None (native Redis) |
{"BACKEND": "redis"/"cache", ...}; see storage examples |
RATELIMIT_REDIS_URL |
None |
Redis URL used when native Redis is selected without an explicit URL/alias |
RATELIMIT_REDIS_TIMEOUT |
1 |
Positive finite number of seconds for Redis network timeouts |
RATELIMIT_FAIL_OPEN |
True |
Allow a request on runtime storage failure when true, otherwise reject |
RATELIMIT_ENABLE_HEADERS |
True |
Add middleware X-RateLimit-* headers |
RATELIMIT_IDENTITY |
"ip" |
"ip", "user", or "custom" |
RATELIMIT_IDENTITY_RESOLVER |
None |
Dotted path to a synchronous callable when custom identities are used |
RATELIMIT_POLICIES |
() |
Up to 64 compiled HTTP policies; higher priority first |
RATELIMIT_EXEMPT_PATHS |
() |
Literal exact paths or slash-terminated subtrees |
RATELIMIT_EXEMPT_IPS |
() |
Client IPs exempt from middleware/ASGI admission |
RATELIMIT_TRUST_X_FORWARDED_FOR |
False |
Enable explicit trusted-proxy XFF parsing |
RATELIMIT_TRUSTED_PROXIES |
() |
Explicit IP addresses/CIDRs for trusted immediate proxy hops |
RATELIMIT_WS_HANDSHAKE |
True |
Enforce a global quota for WebSocket connection handshakes |
RATELIMIT_WS_MSG_RATE |
0 |
Inbound WebSocket message limit; 0 disables |
RATELIMIT_WS_MSG_WINDOW |
60 |
Positive integer seconds for inbound WebSocket quota |
Storage schema: RATELIMIT_STORAGE accepts BACKEND, CACHE_ALIAS, and URL only, with backend-specific restrictions. Cache storage cannot have a URL; native Redis cannot use both URL and CACHE_ALIAS. If using a cache alias, the alias must exist and resolve to a valid Django backend. DummyCache is rejected.
Validation bounds: Rates and windows must be valid integers and within safe Redis numeric limits. Token bucket additionally restricts capacity and window to bounded values (see ratelimit_middleware/constants.py). Booleans, negative/zero windows, unsupported algorithms/identities, misspelled RATELIMIT_* settings, malformed policies, and inconsistent backend options are blocking configuration errors, not warnings.
Exempt paths and IP addresses
RATELIMIT_EXEMPT_PATHS = (
"/health", # Only /health
"/static/", # Subtree; not /static-evil
)
RATELIMIT_EXEMPT_IPS = ("192.0.2.10",)
Set exemptions narrowly. The package rejects ambiguous path declarations and applies client-IP exemptions to the resolved IP. Remember that an exemption is a bypass of this limiter and must not substitute for authorization or network access controls.
Security and deployment considerations
Trusted proxies
X-Forwarded-For is not trusted by default. If the app is behind Nginx or another reverse proxy and client-IP quotas are required:
RATELIMIT_TRUST_X_FORWARDED_FOR = True
RATELIMIT_TRUSTED_PROXIES = (
"10.0.0.0/24", # Example only: replace with the real proxy peer/network
)
The library checks that the immediate socket peer is trusted, then walks the forwarded chain right-to-left to find the first untrusted hop. Malformed/oversized headers use a conservative fallback. Never configure a trust-all network; configure the proxy to normalize/overwrite forwarded headers and prevent direct access to bypass the proxy. Keep trusted ranges as narrow as possible.
Failure policy
RATELIMIT_FAIL_OPEN = True is the default. A temporary storage failure can therefore allow requests without enforcement. Choose False if denial during an outage is preferable to unthrottled traffic; that decision can affect application availability. Fail-open is a global setting, not a per-policy override. Invalid configuration and unsupported Redis Cluster topology are not treated as transient storage failures and should still raise errors.
For security-sensitive endpoints, also apply appropriate controls at the reverse proxy, authentication layer, and network perimeter. Rate limiting does not replace authorization, fraud prevention, or infrastructure-level denial-of-service protection.
Redis Cluster and shared caches
Native Redis grouped admission depends on a single atomic Lua operation across the relevant keys. Redis Cluster multi-key operations spanning different hash slots are unsupported; the library rejects cross-slot/unsupported redirects rather than treating them as ordinary recoverable failures. It does not provide a general Redis Cluster routing/failover adapter.
Django Cache may operate across shared Redis/Memcached instances, but its counter-based algorithm is not an exact sliding window, and eviction or concurrency semantics depend on the actual cache backend. Cache multi-quota decisions are not transactional; they may consume an earlier quota even when a later quota rejects the request.
Authentication boundaries
Authenticated-user limiting must receive a verified request.user or scope["user"] before identity evaluation. Session-authenticated Django requests should place the middleware after AuthenticationMiddleware; DRF JWT/token authentication requires the DRF throttle for verified DRF principals. Custom resolvers are application code and must not accept untrusted identities, block the event loop, or perform expensive lookups per request.
Operational observability
The middleware logs fixed storage-error messages without serializing backend exception details, to avoid leaking credentials. Monitor 429 rates, storage connectivity, and server-side latency at the application/infrastructure layer. No built-in Prometheus metrics exporter is provided by this package.
Performance and testing
The native Redis backend evaluates admission with Lua scripts, avoiding a separate check-and-update round trip. The sliding-window log stores admitted events; token buckets store constant-size state per identity. Actual throughput depends on your Redis deployment, concurrency, and network latency.
Benchmark using a dedicated test Redis instance and choose limits based on production traffic measurements.
For a disposable, isolated Redis instance only:
uv run python scripts/benchmark_ratelimit.py \
--redis-url redis://127.0.0.1:6379/15 \
--confirm-test-redis \
--algorithm token_bucket \
--mode sync \
--requests 10000 \
--workers 32 \
--identities 100
Never run tests or benchmarks against production or other shared Redis instances.
Development
# Install development and optional integration dependencies
uv sync --locked --group dev --extra redis --extra drf
# Unit/integration tests; real backend tests require isolated services and
# RATELIMIT_TEST_REDIS_URL / RATELIMIT_TEST_REDIS5_URL /
# RATELIMIT_TEST_MEMCACHED_URL when applicable.
uv run --no-sync pytest -q
# Django checks
uv run --no-sync python -m django check --settings=tests.settings
# Local developer pre-commit and CI-only validation
uv run --no-sync pre-commit run --all-files
uv run --no-sync pre-commit run --all-files --config .pre-commit-config-ci.yaml
# Compatibility matrix (requires corresponding Python interpreters)
uvx tox
# Build and inspect distribution artifacts
uv build --force-pep517
uv run --no-sync python scripts/check_wheel.py dist/
See CONTRIBUTING.md, CHANGELOG.md, and SECURITY.md.
License
MIT — see LICENSE. Maintained by the Lazarus organization.
Metadata
Release files for dj-ratelimit-middleware 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dj_ratelimit_middleware-1.0.0.tar.gz | 43.7 kB | With provenance | Details for dj_ratelimit_middleware-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dj_ratelimit_middleware-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for dj_ratelimit_middleware-1.0.0-py3-none-any.whl |
Total release size: 85.1 kB
Release files / dj_ratelimit_middleware-1.0.0.tar.gz
| Download URL | dj_ratelimit_middleware-1.0.0.tar.gz |
|---|---|
| Size | 43.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9654d6ef00fbd8b5ee233ef483766f01575b4e702cef225f7d72b5dd0493024a
|
|
BLAKE2b-256 checksum How to use checksums |
05afe52959112fcdf4cdc7dcf303c8d4f69a8e14eb88d39c975e6c961cb2aa35
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.
Transparency logRelease files / dj_ratelimit_middleware-1.0.0-py3-none-any.whl
| Download URL | dj_ratelimit_middleware-1.0.0-py3-none-any.whl |
|---|---|
| Size | 41.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
08f3ab5ddd0672538df6e8b7b0093454929215ba2bce5f7b8570a077866dd878
|
|
BLAKE2b-256 checksum How to use checksums |
9b3a4150362c3eb643481be5497d3ef9a6e35d2a576ba1b7e13ec3f6504ed9bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.
Transparency logSecurity for dj-ratelimit-middleware 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dj_ratelimit_middleware-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 30, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.