bitcoin-core-rpc
A standalone JSON-RPC client against Bitcoin Core.
One source file with nothing but the standard library behind it, fully
annotated and shipping py.typed. BitcoinCoreRpcClient invokes any one
rpc method a node has, with positional or named parameters: one HTTP POST
per call, basic authentication, the result or an exception.
Install it, or copy the file — Vendoring below is how, and it is a supported way to use this rather than a fallback.
pip install bitcoin-core-rpc
Talking to a node
from_chain is the local node of one of Core's chains: the loopback url,
the port and the cookie file all come from Core's own defaults, so a node
started with none of them overridden needs no arguments at all.
from bitcoin_core_rpc import BitcoinCoreRpcClient
client = BitcoinCoreRpcClient.from_chain("main")
print(client.call("getblockcount"))
print(client.call("getblockchaininfo")["chain"])
The credential is the cookie file bitcoind rewrites at every start, read at
each call rather than held: a client built when the node was up still works
an hour and a restart later. Where the datadir is somewhere else — macOS
and Windows put it outside ~/.bitcoin — say so:
from pathlib import Path
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332",
cookie_path=Path.home() / "Library/Application Support/Bitcoin/.cookie",
)
rpcuser and rpcpassword are the other way. They are arguments and never
part of the url: a url with user:password@ in it is refused, because that
string ends up in configuration files, tracebacks and logs.
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332", user="rpcuser", password="rpcpassword"
)
Calling
params is one value, shaped as JSON-RPC shapes it: a sequence for the
positional form, a mapping for the named one. Core takes both, and which
one a method wants is the method's business.
block_id = client.call("getblockhash", [700_000])
block = client.call("getblock", {"blockhash": block_id, "verbosity": 2})
Amounts do not travel as binary floating point in either direction: a
number in the reply decodes as a Decimal, and a Decimal parameter is
refused rather than rounded through float.
balance = client.for_wallet("hot").call("getbalance") # Decimal, exact
for_wallet is the /wallet/<name> endpoint of a node with several
wallets loaded, with the name percent-encoded — a wallet is a directory and
may be called anything a filesystem accepts.
When it goes wrong
Three exceptions, because there are three different things to do about
them. All three are FetchError, so one except FetchError covers the
lot.
| exception | what happened | what it carries |
|---|---|---|
RpcError |
the node computed an error | code, data |
HttpError |
the exchange failed | status |
FetchError |
there was no answer to read | — |
from bitcoin_core_rpc import FetchError, HttpError, RpcError
try:
raw = client.call("getrawtransaction", [tx_id])
except RpcError as e:
if e.code == -5: # no such transaction; a node without -txindex
... # answers this for anything outside its wallet
except HttpError as e:
if e.status == 503: # the rpc work queue is full: try again later
... # a 401 never works again, so tell the two apart
except FetchError:
... # refused connection, expired timeout, no answer
There is no retry, and it is deliberate: call carries any method, so this
client cannot know whether re-sending one is safe, and a timeout is not a
deadline — a node that stopped answering may still be executing the call.
HttpError.status is what makes a caller's own policy three lines rather
than a match on the text of a message.
What it does not do
- batches. One call is one HTTP request. A batch needs an api for
correlating the answers and for partly failing, which is a question of
its own; a loop over
callis the replacement, an equivalent beside the node and not over a link where the round trip costs something. - notifications, a request sent with no
id, which a node does not answer. - retries, per above.
- redirects. A 30x arrives as an
HttpErrorrather than as a second request: the first one already carries theAuthorizationfor the host it names, and following the redirect would send that credential wherever theLocationpoints. - proxies from the environment.
HTTP_PROXYis set for a browser or a package manager and inherited by everything in the shell, which is the wrong source for the decision of where a wallet command is sent. A caller who does want one passes atransport.
Migrating from AuthServiceProxy
It is not python-bitcoinrpc's AuthServiceProxy and not a port of it.
That class, and the copy of it Core's test framework maintains, carry the
LGPL-2.1 of their python-jsonrpc ancestry, where this is MIT: this is an
implementation of the protocol and shares no line with either. What a
caller rewrites is below, an AuthServiceProxy line at a time with the
same command under it.
Connecting. The credential is an argument, and a url carrying
user:password@ is refused rather than accepted and stripped: that url is
the string that ends up in a configuration file, a traceback and a log.
# AuthServiceProxy
rpc = AuthServiceProxy(f"http://{user}:{password}@127.0.0.1:8332")
# this client
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332", user=user, password=password
)
A node left on its defaults needs neither argument: from_chain has the
port and the cookie file, per Talking to a node.
Invoking a method. The method is an argument and not an attribute: any
name a node has works without this class knowing it, and none of them can
collide with a name of the client's own. params is then one argument as
well, a sequence for the positional form and a mapping for the named one.
# AuthServiceProxy
block = rpc.getblock(block_id, 2)
# this client
block = client.call("getblock", [block_id, 2])
block = client.call("getblock", {"blockhash": block_id, "verbosity": 2})
A wallet command. /wallet/<name> is derived from the client rather
than written into a second url, and the name is percent-encoded.
# AuthServiceProxy
hot = AuthServiceProxy(f"http://{user}:{password}@127.0.0.1:8332/wallet/hot")
balance = hot.getbalance()
# this client
balance = client.for_wallet("hot").call("getbalance")
A batch. There is none, per What it does not do, and a loop is what replaces it: the calls go one HTTP request each rather than several in one, and each answer is a value or an exception where a batch answered with a list to inspect.
# AuthServiceProxy
hashes = rpc.batch_([["getblockhash", height] for height in heights])
# this client
hashes = [client.call("getblockhash", [height]) for height in heights]
An error. JSONRPCException becomes three exceptions, and When it
goes wrong above has the table: RpcError for an
error the node computed, HttpError for an exchange that failed and
FetchError for no answer to read. All three are FetchError, so except FetchError is the translation that catches what the one exception caught.
COMPARISON.md has the case for the switch beyond this rewrite guide, and why three features this client does not have are decisions rather than omissions.
Testing code that calls a node
transport is the seam, and it is public for this: a callable taking the
request and a timeout, answering with the HTTP status and the body. The
suite of this project opens no socket, and neither has yours to.
import json
def transport(request, timeout):
request_id = json.loads(request.data)["id"]
body = {"jsonrpc": "2.0", "id": request_id, "result": 481824}
return 200, json.dumps(body).encode()
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332", user="u", password="p", transport=transport
)
assert client.call("getblockcount") == 481824
What a transport of your own owes, none of which this module can check for
it: its own bound on what it holds in memory while reading, its own bound
on how long it holds the call — most client libraries spend timeout per
socket operation, which a peer dripping a body resets forever — no
redirect followed, and its own thread safety.
Type checking
The source is annotated throughout, mypy --strict runs over it here, and
the distribution ships py.typed — so your own checker reads those
annotations with no configuration of any kind:
$ mypy --strict your_code.py
your_code.py:4: error: Argument 1 to "call" of "BitcoinCoreRpcClient" has
incompatible type "int"; expected "str"
That marker is why the source is bitcoin_core_rpc/__init__.py and not a
top-level module: PEP 561 puts it inside a package directory and nowhere
else. pyproject.toml records what the alternatives were measured to do.
Vendoring
Copy bitcoin_core_rpc/__init__.py whole from a release tag, rename it
to bitcoin_core_rpc.py, keep the license notice at the top of it — MIT,
embedded rather than referenced, because a copy has no LICENSE beside it
— and record the tag next to the copy. An update is a replacement of the
whole file, and this shows every behavioral change first:
git diff OLD..NEW -- bitcoin_core_rpc/__init__.py
A vendored copy receives no security or compatibility fix automatically, so its recorded tag is what says whether it needs replacing. An installed one is a version an ordinary dependency bump moves, which is why installing is the default advice.
Security
Basic authentication is cleartext over plain HTTP, that being what Core's rpc speaks. On loopback that cleartext is between one process and the node beside it; for a node anywhere else it is on the wire, and rpc credentials authorise every wallet command that node has. SECURITY.md carries the rest, and how to report a vulnerability.
Contributing
CONTRIBUTING.md has the commands each CI job runs,
verbatim. uv sync creates the environment; uv is the only tool that has
to be installed.
Links
- Documentation: https://bitcoin-core-rpc.readthedocs.io/
- Source: https://github.com/btclib-org/bitcoin-core-rpc
- Releases: https://github.com/btclib-org/bitcoin-core-rpc/releases
- CHANGELOG.md, and HISTORY.md for what a release asks a user to act on
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bitcoin_core_rpc-2026.8.8.tar.gz.
File metadata
- Download URL: bitcoin_core_rpc-2026.8.8.tar.gz
- Upload date:
- Size: 329.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd14d878a26bd66bc13c0501c04cfe1736b2e84636f93f2fb92265a797f18403
|
|
| MD5 |
06e059415df762740192beac732f42b2
|
|
| BLAKE2b-256 |
69353b2e4af1a6e14d2c6b55d727dddb38a4c16e7498a8545ff00e259b5389fe
|
Provenance
The following attestation bundles were made for bitcoin_core_rpc-2026.8.8.tar.gz:
Publisher:
release.yml on btclib-org/bitcoin-core-rpc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bitcoin_core_rpc-2026.8.8.tar.gz -
Subject digest:
dd14d878a26bd66bc13c0501c04cfe1736b2e84636f93f2fb92265a797f18403 - Sigstore transparency entry: 2387108214
- Sigstore integration time:
-
Permalink:
btclib-org/bitcoin-core-rpc@6eb67e3bc8ef16f9537f55fec48bd987ce26885e -
Branch / Tag:
refs/tags/v2026.8.8 - Owner: https://github.com/btclib-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6eb67e3bc8ef16f9537f55fec48bd987ce26885e -
Trigger Event:
push
-
Statement type:
File details
Details for the file bitcoin_core_rpc-2026.8.8-py3-none-any.whl.
File metadata
- Download URL: bitcoin_core_rpc-2026.8.8-py3-none-any.whl
- Upload date:
- Size: 37.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61dc813b3d7bf417eb02fc8742f27d4a1054801fde5e16de7396ee020992ea1b
|
|
| MD5 |
78a5ab46c58286bdcf2bb678c5bfe362
|
|
| BLAKE2b-256 |
72894cbaa77aaed3bd080f109054a3439cfecdd96a3d86029cbd598928320627
|
Provenance
The following attestation bundles were made for bitcoin_core_rpc-2026.8.8-py3-none-any.whl:
Publisher:
release.yml on btclib-org/bitcoin-core-rpc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bitcoin_core_rpc-2026.8.8-py3-none-any.whl -
Subject digest:
61dc813b3d7bf417eb02fc8742f27d4a1054801fde5e16de7396ee020992ea1b - Sigstore transparency entry: 2387108218
- Sigstore integration time:
-
Permalink:
btclib-org/bitcoin-core-rpc@6eb67e3bc8ef16f9537f55fec48bd987ce26885e -
Branch / Tag:
refs/tags/v2026.8.8 - Owner: https://github.com/btclib-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6eb67e3bc8ef16f9537f55fec48bd987ce26885e -
Trigger Event:
push
-
Statement type: