pyperclip
pyperclip is a single-module package with two functions that matter: copy(text) puts a string on the system clipboard and paste() reads it back. Underneath it picks a mechanism for your platform the first time you call either one. On Windows it calls the Win32 clipboard API through ctypes with no extra install. On macOS it uses pyobjc if that happens to be importable and otherwise shells out to pbcopy and pbpaste. On Linux it looks for wl-copy and wl-paste under Wayland, then xclip, then xsel, then klipper over qdbus, and finally tries qtpy or PyQt5. WSL gets its own path. If nothing is found it installs stub functions that raise PyperclipException the moment you use them. It handles plain text only, and that is the whole library.
For putting a string on a desktop clipboard from a Python script, it is two functions and no dependencies, and that is exactly right. Treat every call as something that can raise on Linux, and do not reach for it on a server.
Use it if
- You are writing a desktop-side CLI or script and want a one-liner to hand a result to the user's clipboard instead of making them select terminal output
- You need it to work on Windows, macOS, and a desktop Linux without shipping platform branches, and you accept a runtime error where no clipboard exists
- You want zero dependencies: the package is a single pure-Python file with no install-time requirements at all
- You are teaching or prototyping, where import pyperclip and two function calls is the entire mental model
- You want a clipboard from the shell too, since python -m pyperclip -c reads stdin onto the clipboard and -p writes it back out
- You are running on a server, in Docker, in CI, or over plain SSH: there is no clipboard there, so the first copy() raises PyperclipException and no amount of pip installing fixes it
- You need anything other than plain text: images, HTML, RTF, and file lists are all out of scope, and the PyPI summary says as much
- You care about the PATH your process runs with: the module docstring carries a security note that it invokes which, pbcopy, pbpaste, xclip, xsel, wl-copy, wl-paste, klipper, and qdbus by name, so anything earlier on PATH with those names runs with your process's permissions
- You want to watch the clipboard for changes: there is no monitoring API in 1.11.0, whose __all__ is just copy, paste, set_clipboard, and determine_clipboard, so a polling loop is your only option
- You copy in a tight loop on Linux or macOS: each call spawns a subprocess through Popen, which is milliseconds of process startup per copy rather than a library call
- You need active maintenance: 1.11.0 shipped 2025-09-26 and the repository has not been pushed since that day, with roughly 116 issues open
- You type-check strictly: no py.typed marker ships in the wheel, so mypy and pyright see an untyped module until you add the separate types-pyperclip stubs
- You are on Cygwin: the module says outright that Cygwin is not supported and warns if it falls back to /dev/clipboard
Setup reality
pip install pyperclip finishes instantly and installs one file with no dependencies. On Windows that is genuinely all of it. On macOS it works out of the box because pbcopy and pbpaste are part of the OS. Linux is where the install lies to you: pip succeeds, then the first copy() raises PyperclipException telling you to apt-get install xclip, because pyperclip needs an external binary it cannot declare as a Python dependency. Which one depends on the session, since Wayland wants wl-clipboard while X11 wants xclip or xsel, and pyperclip decides at runtime by checking WAYLAND_DISPLAY and DISPLAY. Headless environments have neither, so a Docker image or a CI job needs xvfb plus xclip if you truly must exercise this path, and skipping the test is usually the better answer. Two more details worth planning for. Detection is lazy, so copy and paste are stub functions until the first call, which means an import-time availability check has to call is_available() or determine_clipboard() explicitly. And on X11 the clipboard is owned by a process rather than the system, so text copied by a short-lived script can vanish when the owner exits, which is a property of X11 and not something pyperclip can fix.
Patterns
Put text on the clipboard and read it backcopy-and-paste
import pyperclip
pyperclip.copy('the text to be copied')
print(pyperclip.paste())
# 'the text to be copied'The backend is chosen lazily on this first call, not at import, so the import itself never fails even on a machine with no clipboard at all.
Fail with a message instead of a tracebackhandle-no-clipboard
import sys
import pyperclip
def to_clipboard(text: str) -> bool:
try:
pyperclip.copy(text)
return True
except pyperclip.PyperclipException as exc:
print(f'clipboard unavailable: {exc}', file=sys.stderr)
return False
if not to_clipboard(result):
print(result) # fall back to stdoutPyperclipException subclasses RuntimeError. On Linux with no mechanism installed the message already tells the user to install xclip or wl-clipboard, so printing it is more useful than wrapping it. Falling back to stdout is what makes a CLI usable over SSH.
Detect clipboard support up frontcheck-availability
import pyperclip
pyperclip.determine_clipboard() # force detection now
if not pyperclip.is_available():
print('no clipboard on this machine; use --output instead')is_available() just checks whether copy and paste are still the lazy-loading stubs, so it always reports True until something triggers detection. Calling determine_clipboard() first is what makes the answer meaningful.
Pin a specific clipboard mechanismforce-backend
import pyperclip
# valid: pbcopy, pyobjc, qt, xclip, xsel, wl-clipboard, klipper, windows, no
pyperclip.set_clipboard('xsel')
pyperclip.copy('hello')
# 'no' turns every call into a raising stub, handy in tests
pyperclip.set_clipboard('no')Call this before the first copy or paste, since detection is what set_clipboard replaces. An unknown name raises ValueError listing the valid options, and picking a backend whose binary is missing fails later at call time rather than here.
Find out which mechanism was pickedinspect-backend
import pyperclip
copy_fn, paste_fn = pyperclip.determine_clipboard()
print(copy_fn.__name__) # e.g. 'copy_xclip', 'copy_wl', 'copy_windows', 'ClipboardUnavailable'The function name is the only handle you get on the chosen backend; there is no attribute reporting it. Worth logging in a bug report template, because 'works on my machine' clipboard problems are almost always a different backend.
Install the binary Linux actually needslinux-prerequisites
# Wayland session (echo $WAYLAND_DISPLAY is non-empty)
$ sudo apt-get install wl-clipboard
# X11 session (echo $DISPLAY is non-empty)
$ sudo apt-get install xclip # preferred
$ sudo apt-get install xsel # fallback
# nothing else installed? pyperclip tries qtpy, then PyQt5
$ pip install qtpy PyQt5Detection order is wl-clipboard, then xclip, then xsel, then klipper over qdbus, then Qt. pyperclip cannot declare these as Python dependencies, which is why pip install succeeds on a box where the library cannot work.
Keep clipboard code out of CIheadless-environment
import os
import pytest
import pyperclip
pyperclip.determine_clipboard()
@pytest.mark.skipif(not pyperclip.is_available(), reason='no clipboard')
def test_copies_result():
pyperclip.copy('x')
assert pyperclip.paste() == 'x'
# if you really must exercise it in a container:
# apt-get install -y xvfb xclip
# xvfb-run -a pytestContainers and CI runners have no DISPLAY and no WAYLAND_DISPLAY, so pyperclip lands on the stub backend and every call raises. Skipping is nearly always cheaper than standing up xvfb just to test two lines of glue.
Test the code around the clipboardmock-in-tests
from unittest import mock
def test_command_copies_output():
buffer = {}
with mock.patch('pyperclip.copy', side_effect=lambda t: buffer.update(text=t)), \
mock.patch('pyperclip.paste', side_effect=lambda: buffer.get('text', '')):
run_command(['export', '--clip'])
assert buffer['text'].startswith('id,name')Patch pyperclip.copy and pyperclip.paste rather than the backend, because the module rebinds those module-level names during lazy loading and a patch applied too early can be overwritten by detection.
Copy something that is not already a stringcopy-non-string
import json
import pyperclip
pyperclip.copy(json.dumps(payload, indent=2))
pyperclip.copy(str(row_count))
# pyperclip.copy(42) happens to work on most backends, but do not rely on it
# pyperclip.copy(None) puts the literal text 'None' on the clipboardBackends call str() on whatever you pass, so nothing raises and a bug becomes the string 'None' sitting on the user's clipboard. Serialize explicitly and you keep control of the formatting.
Poll the clipboard for new contentwatch-for-changes
import time
import pyperclip
def watch(interval: float = 0.5):
previous = pyperclip.paste()
while True:
current = pyperclip.paste()
if current != previous:
previous = current
yield current
time.sleep(interval)
for text in watch():
print('clipboard changed:', text[:60])There is no wait or notify API in 1.11.0, so polling is the whole story. On Linux and macOS each paste() spawns a subprocess, so a 0.5 second interval is roughly two process launches a second; do not tighten it much below that.
Wipe a secret off the clipboardclear-clipboard
import threading
import pyperclip
def copy_secret(secret: str, seconds: int = 20) -> None:
pyperclip.copy(secret)
def clear():
if pyperclip.paste() == secret:
pyperclip.copy('')
threading.Timer(seconds, clear).start()Copying an empty string is the only clear there is. The equality check avoids wiping something the user copied in the meantime. On X11 the selection lives in the process that set it, so a script that exits before the timer fires may have already lost the value anyway.
Use it as a shell commandcli-usage
$ echo 'hello' | python -m pyperclip --copy
$ python -m pyperclip --copy 'literal text'
$ python -m pyperclip --paste > out.txt
# short forms
$ cat report.csv | python -m pyperclip -c
$ python -m pyperclip -p | wc -lThe module has a small __main__, so no console script is installed and it is python -m pyperclip rather than a pyperclip command. With --copy and no argument it reads all of stdin, so pressing Ctrl-D is what ends an interactive run.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pyclip | PyPI | You need binary clipboard data as well as text, with the same tiny cross-platform surface |
| klembord | PyPI | You need rich content on the clipboard, such as HTML alongside a plain text fallback |
| pyperclip3 | PyPI | You want a pyperclip-shaped API that also handles bytes, without switching to a different function naming style |