Claude Code for portalocker: Cross-Platform File Locking — Claude Skills 360 Blog
Blog / AI / Claude Code for portalocker: Cross-Platform File Locking
AI

Claude Code for portalocker: Cross-Platform File Locking

Published: March 9, 2028
Read time: 5 min read
By: Claude Skills 360

portalocker wraps OS file locking (flock on POSIX, LockFile on Windows) with a clean Python API. pip install portalocker. Exclusive: import portalocker; portalocker.lock(fh, portalocker.LOCK_EX) — blocks until exclusive lock acquired. Shared: portalocker.lock(fh, portalocker.LOCK_SH) — multiple readers, no writers. Non-blocking: portalocker.LOCK_EX | portalocker.LOCK_NB — raises LockException immediately if locked. Unlock: portalocker.unlock(fh). Context manager: with portalocker.Lock("file.txt", "r") as fh:. Write: with portalocker.Lock("file.txt", "w", timeout=5) as fh: fh.write(data). Timeout: portalocker.Lock("file.txt", timeout=10, check_interval=0.1). RLock: portalocker.RLock("file.txt") — reentrant, same process can acquire multiple times. Semaphore: portalocker.BoundedSemaphore(2, "sem.txt") — allow N concurrent processes. Exception: portalocker.LockException (for LOCK_NB fail). portalocker.AlreadyLocked. Truncate: portalocker.Lock("file.txt", mode="w", truncate=True). flags=0 — no flags, use portalocker.LOCK_EX by default. PID file: lock a .pid file for singleton process guard. portalocker.lock(fh, portalocker.LOCK_EX | portalocker.LOCK_NB) in startup. Claude Code generates portalocker read-write guards, semaphore pools, and PID file patterns.

CLAUDE.md for portalocker

## portalocker Stack
- Version: portalocker >= 2.8 | pip install portalocker
- Exclusive: portalocker.lock(fh, portalocker.LOCK_EX) | LOCK_SH for shared read
- Non-blocking: LOCK_EX | LOCK_NB — raises LockException immediately
- Class: portalocker.Lock("file.txt", "a+", timeout=10) context manager
- RLock: portalocker.RLock — reentrant lock, same process can acquire multiple times
- Semaphore: portalocker.BoundedSemaphore(n, "sem.lock") — n concurrent processes
- Exceptions: portalocker.LockException | portalocker.AlreadyLocked

portalocker Cross-Platform File Locking Pipeline

# app/port_locking.py — portalocker exclusive/shared locks, semaphore, and PID files
from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path
from typing import Any

import portalocker


# ─────────────────────────────────────────────────────────────────────────────
# 1. Low-level lock/unlock wrappers
# ─────────────────────────────────────────────────────────────────────────────

def exclusive_lock(fh, blocking: bool = True) -> None:
    """
    Acquire an exclusive (write) lock on an open file handle.
    blocking=False: raises portalocker.LockException immediately if locked.
    """
    flags = portalocker.LOCK_EX
    if not blocking:
        flags |= portalocker.LOCK_NB
    portalocker.lock(fh, flags)


def shared_lock(fh, blocking: bool = True) -> None:
    """
    Acquire a shared (read) lock.
    Multiple processes can hold shared locks simultaneously.
    Any exclusive lock request will block until all shared locks are released.
    """
    flags = portalocker.LOCK_SH
    if not blocking:
        flags |= portalocker.LOCK_NB
    portalocker.lock(fh, flags)


def release_lock(fh) -> None:
    """Release any lock held on the file handle."""
    portalocker.unlock(fh)


# ─────────────────────────────────────────────────────────────────────────────
# 2. High-level Lock class wrappers
# ─────────────────────────────────────────────────────────────────────────────

def locked_write(
    path: str | Path,
    data: str,
    timeout: float = 30.0,
    encoding: str = "utf-8",
) -> None:
    """
    Write text to a file with an exclusive lock.
    Safe for concurrent writes from multiple processes.
    """
    lock = portalocker.Lock(
        str(path),
        mode="w",
        timeout=timeout,
        encoding=encoding,
        flags=portalocker.LOCK_EX,
    )
    with lock as fh:
        fh.write(data)


def locked_append(
    path: str | Path,
    data: str,
    timeout: float = 30.0,
    encoding: str = "utf-8",
) -> None:
    """
    Append text to a file with an exclusive lock.
    Multiple processes appending concurrently are serialized.
    """
    lock = portalocker.Lock(
        str(path),
        mode="a",
        timeout=timeout,
        encoding=encoding,
        flags=portalocker.LOCK_EX,
    )
    with lock as fh:
        if not data.endswith("\n"):
            data += "\n"
        fh.write(data)


def locked_read(
    path: str | Path,
    timeout: float = 30.0,
    encoding: str = "utf-8",
) -> str:
    """
    Read text from a file with a shared lock.
    Blocks writers while multiple readers operate concurrently.
    """
    lock = portalocker.Lock(
        str(path),
        mode="r",
        timeout=timeout,
        encoding=encoding,
        flags=portalocker.LOCK_SH,
    )
    with lock as fh:
        return fh.read()


def try_locked_write(
    path: str | Path,
    data: str,
    encoding: str = "utf-8",
) -> bool:
    """
    Non-blocking exclusive write.
    Returns True on success, False if file is already locked.
    """
    try:
        lock = portalocker.Lock(
            str(path),
            mode="w",
            timeout=0,
            encoding=encoding,
            flags=portalocker.LOCK_EX | portalocker.LOCK_NB,
        )
        with lock as fh:
            fh.write(data)
        return True
    except portalocker.LockException:
        return False


# ─────────────────────────────────────────────────────────────────────────────
# 3. Read-modify-write (atomic JSON update)
# ─────────────────────────────────────────────────────────────────────────────

def json_update(
    path: str | Path,
    update_fn,
    timeout: float = 30.0,
) -> dict[str, Any]:
    """
    Atomically read, update, and write back a JSON file.
    Uses exclusive lock + truncate to replace content safely.
    """
    path = Path(path)

    # Open for reading AND writing (r+ keeps existing content, a+ appends)
    # We open with "a+" to create if not exists, then seek to beginning to read
    with portalocker.Lock(
        str(path),
        mode="a+",
        timeout=timeout,
        encoding="utf-8",
        flags=portalocker.LOCK_EX,
    ) as fh:
        fh.seek(0)
        content = fh.read()
        data    = json.loads(content) if content.strip() else {}
        updated = update_fn(data)

        fh.seek(0)
        fh.truncate()
        json.dump(updated, fh, indent=2, ensure_ascii=False)

    return updated


# ─────────────────────────────────────────────────────────────────────────────
# 4. PID file / singleton guard
# ─────────────────────────────────────────────────────────────────────────────

class PidGuard:
    """
    Prevent duplicate process instances using a PID file + exclusive lock.
    The lock is held open for the lifetime of the guarded process.
    When the process exits (even on crash), the OS releases the flock.
    """

    def __init__(self, name: str, pid_dir: str = "/tmp") -> None:
        self._pid_path = Path(pid_dir) / f"{name}.pid"
        self._fh       = None

    def acquire(self) -> bool:
        """
        Acquire the singleton guard.
        Returns True if this process is now sole owner; False otherwise.
        """
        try:
            self._fh = open(self._pid_path, "a+")
            portalocker.lock(self._fh, portalocker.LOCK_EX | portalocker.LOCK_NB)
        except portalocker.LockException:
            if self._fh:
                self._fh.close()
                self._fh = None
            return False

        # Write PID for diagnostics
        self._fh.seek(0)
        self._fh.truncate()
        self._fh.write(str(os.getpid()))
        self._fh.flush()
        return True

    def release(self) -> None:
        if self._fh:
            portalocker.unlock(self._fh)
            self._fh.close()
            self._fh = None

    def __enter__(self) -> "PidGuard":
        if not self.acquire():
            raise RuntimeError(
                f"Another instance is already running (see {self._pid_path})"
            )
        return self

    def __exit__(self, *args) -> None:
        self.release()


# ─────────────────────────────────────────────────────────────────────────────
# 5. BoundedSemaphore — limit N concurrent processes
# ─────────────────────────────────────────────────────────────────────────────

def bounded_task(name: str, sem_path: str, max_workers: int, timeout: float = 60.0):
    """
    Context manager that limits concurrent executions via BoundedSemaphore.
    At most `max_workers` processes can be inside the `with` block at once.
    """
    return portalocker.BoundedSemaphore(
        maximum=max_workers,
        filename=sem_path,
        timeout=timeout,
    )


# ─────────────────────────────────────────────────────────────────────────────
# 6. RLock — reentrant lock
# ─────────────────────────────────────────────────────────────────────────────

def get_rlock(path: str | Path, timeout: float = 30.0) -> portalocker.RLock:
    """
    Return a reentrant file lock.
    The same process can acquire the lock multiple times without deadlocking.
    Inner acquisitions are no-ops; the lock is released only on the outermost release.
    """
    return portalocker.RLock(str(path), timeout=timeout)


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import multiprocessing
    import time

    with tempfile.TemporaryDirectory() as tmpdir:
        counter_path = Path(tmpdir) / "counter.txt"
        counter_path.write_text("0")

        def safe_increment(n: int) -> None:
            for _ in range(n):
                with portalocker.Lock(
                    str(counter_path),
                    mode="a+",
                    timeout=10,
                    flags=portalocker.LOCK_EX,
                ) as fh:
                    fh.seek(0)
                    val = int(fh.read() or "0")
                    fh.seek(0)
                    fh.truncate()
                    fh.write(str(val + 1))
                time.sleep(0.001)

        print("=== Concurrent counter (4 processes × 5 increments) ===")
        with multiprocessing.Pool(4) as pool:
            pool.map(safe_increment, [5] * 4)
        print(f"  Expected: 20 | Got: {counter_path.read_text()} | "
              f"Correct: {counter_path.read_text() == '20'}")

        print("\n=== json_update ===")
        json_path = Path(tmpdir) / "store.json"

        def add_entry(data: dict) -> dict:
            data["count"] = data.get("count", 0) + 1
            return data

        for _ in range(4):
            result = json_update(json_path, add_entry)
        print(f"  After 4 updates: {result}")

        print("\n=== BoundedSemaphore (max 2 at once) ===")
        sem_path = str(Path(tmpdir) / "sem.lock")
        arrivals = multiprocessing.Manager().list()

        def sem_task(i: int) -> None:
            with bounded_task(f"worker-{i}", sem_path, max_workers=2, timeout=30):
                arrivals.append(os.getpid())
                time.sleep(0.05)

        with multiprocessing.Pool(4) as pool:
            pool.map(sem_task, range(4))
        print(f"  {len(arrivals)} tasks completed through semaphore")

        print("\n=== PidGuard (singleton) ===")
        guard = PidGuard("test", pid_dir=tmpdir)
        acquired = guard.acquire()
        second   = PidGuard("test", pid_dir=tmpdir).acquire()
        print(f"  First acquire: {acquired} | Second (should be False): {second}")
        guard.release()
        third = PidGuard("test", pid_dir=tmpdir).acquire()
        print(f"  After release (should be True): {third}")
        PidGuard("test", pid_dir=tmpdir).release()

For the filelock alternative — filelock and portalocker both provide cross-platform file locking; filelock is lighter (one dependency, simple API) while portalocker adds LOCK_SH shared reads, RLock for reentrant locking, and BoundedSemaphore for N-concurrency patterns; choose filelock for simple exclusive-lock use cases and portalocker when you need shared read locks or the semaphore primitive. For the fcntl.flock alternative — fcntl.flock is POSIX-only and raises NotImplementedError on Windows; portalocker abstracts this behind a platform-independent API using LockFile on Windows and flock + lockf on POSIX, so the same portalocker.lock(fh, LOCK_EX) call works on both. The Claude Skills 360 bundle includes portalocker skill sets covering portalocker.lock() with LOCK_EX/LOCK_SH/LOCK_NB flags, portalocker.Lock context manager with timeout, locked_write() and locked_append() helpers, locked_read() with shared lock, try_locked_write() non-blocking attempt, json_update() read-modify-write pattern, PidGuard singleton process guard, bounded_task() BoundedSemaphore context manager, and portalocker.RLock reentrant locking. Start with the free tier to try cross-platform file locking code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free