Skip to main content

A Python client for interacting with the WordPress REST API.

Project description

wp-python

A Python 3.12 client for the WordPress REST API.

Installation

pip install wp-python

Quick start

from wp_python import WordPressClient, ApplicationPasswordAuth

auth = ApplicationPasswordAuth("username", "xxxx xxxx xxxx xxxx xxxx xxxx")
client = WordPressClient("https://your-site.com", auth=auth)

# Standard endpoints
posts = client.posts.list()
me    = client.users.me()
page  = client.pages.get(5)

# Custom post types
products = client.custom_post_type("products")
items    = products.list(per_page=20, status="publish")
item     = products.get(42)
new_item = products.create({"title": "Widget", "status": "publish"})
products.update(42, {"title": "Updated Widget"})
products.delete(42, force=True)

Project structure

src/wp_python/
├── __init__.py           # Public exports
├── client.py             # WordPressClient + CustomPostTypeEndpoint
├── auth.py               # Auth handlers
├── exceptions.py         # Typed exceptions
├── transport.py          # HTTP layer (HttpxTransport, Transport protocol)
├── paginated_result.py   # PaginatedResult container
├── models/               # Pydantic models (Post, Page, User, …)
└── endpoints/            # Typed endpoint classes (posts, users, …)

Authentication

WordPress Application Passwords (recommended for the REST API):

  1. In WordPress admin go to Users → Profile → Application Passwords
  2. Generate a password for your app
  3. Use it with ApplicationPasswordAuth:
from wp_python import ApplicationPasswordAuth, WordPressClient

auth = ApplicationPasswordAuth("andrew", "naAg I4sg dwFI R9PC V06P 1a1o")
client = WordPressClient("https://example.com", auth=auth)

Other supported auth types: BasicAuth, JWTAuth, OAuth2Auth.

Typed endpoints

Standard WordPress resources are exposed as typed endpoints on the client. All list() methods return a PaginatedResult — a list-like object that also carries total, total_pages, has_next, and has_prev:

result = client.posts.list(per_page=10)

for post in result:
    print(post.title.rendered)

if result.has_next:
    next_page = client.posts.list(page=result.page + 1)

print(f"{len(result)} of {result.total} posts")

Use iterate_all() to page through everything without managing page numbers:

for post in client.posts.iterate_all(per_page=100):
    print(post.id)

Custom post types

client.custom_post_type(slug) returns a CustomPostTypeEndpoint that supports the same CRUD operations but returns raw dict objects (since the schema is unknown at construction time):

cpt   = client.custom_post_type("restart-registry")
posts = cpt.list(author=1, status="any")   # list[dict]
post  = cpt.get(13)                         # dict
new   = cpt.create({"title": "My Registry", "status": "publish"})
upd   = cpt.update(13, {"status": "private"})
cpt.delete(13, force=True)

Embedding linked resources (embed option)

WordPress's _embed query parameter tells the REST API to inline linked resources — such as the author object — directly in the response body under _embedded, saving extra round-trips.

Pass embed at construction time to inject _embed into every request made through that endpoint (list, get, create, and update):

cpt = client.custom_post_type("restart-registry", embed="author")

posts = cpt.list()    # GET /wp/v2/restart-registry?_embed=author&…
post  = cpt.get(13)   # GET /wp/v2/restart-registry/13?_embed=author
new   = cpt.create() # POST /wp/v2/restart-registry?_embed=author
upd   = cpt.update() # PUT  /wp/v2/restart-registry/13?_embed=author

The author slug is then available at:

post["_embedded"]["author"][0]["slug"]

embed accepts the same values as the WordPress _embed query parameter:

Value Effect
"author" Embed author object only
"wp:term" Embed taxonomy terms only
True Embed all linked resources
None (default) No embedding; standard response

WordPress has supported _embed on write operations (POST/PUT) since 5.4. delete() intentionally never sends _embed; the deletion response body does not include linked resources regardless.

A per-call kwarg takes precedence over the endpoint-level default:

cpt = client.custom_post_type("products", embed="author")
cpt.list(_embed="wp:term")  # sends _embed=wp:term, not author

Design note — why endpoint-level rather than per-call?

For: The primary motivation is eliminating N identical kwarg repetitions across every call site when a project consistently needs linked data from a specific CPT. In restart-lambda, for example, five separate calls to a restart-registry endpoint all need _embed=author to resolve the registry owner's username. Setting it once at construction keeps call sites clean and removes a class of bug where a new call site forgets the kwarg.

Against: Endpoint-level state is invisible at the call site. A reader seeing cpt.get(42) has no immediate signal that the response will contain _embedded data. Per-call kwargs (cpt.get(42, _embed="author")) are more explicit and consistent with how every other optional WP REST parameter is passed through **kwargs. They also avoid the edge case where a single endpoint instance is shared and some calls genuinely should not embed.

The per-call override mechanism (last example above) exists precisely because endpoint-level defaults are not always right for every call. If your code has only one or two CPT call sites that need embed, prefer per-call kwargs instead.

Error handling

from wp_python.exceptions import (
    AuthenticationError,  # 401
    PermissionError,      # 403
    NotFoundError,        # 404
    ValidationError,      # 400
    RateLimitError,       # 429
    ServerError,          # 5xx
    WordPressError,       # base class
)

try:
    post = client.posts.get(99999)
except NotFoundError:
    print("Post not found")
except PermissionError:
    print("Not authorised")
except WordPressError as e:
    print(f"API error {e.status_code}: {e.message}")

Context manager

WordPressClient implements __enter__ / __exit__ and can be used as a context manager to ensure the underlying connection pool is always closed:

with WordPressClient("https://example.com", auth=auth) as client:
    posts = client.posts.list()

Transport layer

HTTP logic lives in HttpxTransport, separate from WordPressClient and the endpoint classes. You can wrap it to add retry behaviour, logging, or swap in a fake for tests:

from wp_python.transport import HttpxTransport

class RetryTransport:
    def __init__(self, inner, max_retries=3): ...
    def request(self, method, path, **kwargs): ...
    def close(self): ...

client.transport = RetryTransport(client.transport)

Dependencies

  • httpx — HTTP client
  • pydantic — data validation and model serialisation

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wp_python-0.1.5.tar.gz (26.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wp_python-0.1.5-py3-none-any.whl (4.7 kB view details)

Uploaded Python 3

File details

Details for the file wp_python-0.1.5.tar.gz.

File metadata

  • Download URL: wp_python-0.1.5.tar.gz
  • Upload date:
  • Size: 26.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wp_python-0.1.5.tar.gz
Algorithm Hash digest
SHA256 89350495ab6c2ec00b817a4bf84afd76ca6e33b5e7c4ef9188daabec1ce6cd1e
MD5 b62b8ed9c55c93e6efabcf527bab65c7
BLAKE2b-256 7e73da8777171a46648a9eb075f044735db2c7574bc89460641ea888fc734d67

See more details on using hashes here.

File details

Details for the file wp_python-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: wp_python-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 4.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wp_python-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 a695e512dabe27385e0645fd6de91d0ea4a60be9dda09b628016a34d51054756
MD5 7f3b4103d04756ed0789b0a4c469f5bb
BLAKE2b-256 bec2f2a07ed6042bd0606e9e0a6423ce52c85b82a309ba518d2d5eec7084b91d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page