Skip to content
Draft

v1 #673

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
743063c
add a section for v1 release notes
Nov 6, 2023
1df6867
Merge branch 'main' into v1
Nov 6, 2023
3ac5f4d
Merge branch 'main' into v1
Nov 7, 2023
63a3e60
make PythonCall.GC more like Base.GC (#413)
cjdoris Nov 7, 2023
d0cc3f7
Merge branch 'main' into v1
Nov 7, 2023
bd7e81a
Merge branch 'main' into v1
Mar 3, 2024
19119b6
Merge branch 'main' into v1
Mar 17, 2024
b81c389
Merge remote-tracking branch 'origin/main' into v1
May 4, 2024
63b2801
Merge remote-tracking branch 'origin/main' into v1
May 18, 2024
e8ebcea
Merge remote-tracking branch 'origin/main' into v1
Jun 10, 2024
eda0cc6
pyjl (#512)
cjdoris Jun 21, 2024
37a3c85
Merge remote-tracking branch 'origin/main' into v1
Aug 15, 2024
02bed07
fix tests for v1
Aug 15, 2024
2b0d376
Merge remote-tracking branch 'origin/main' into v1
Aug 15, 2024
4fa1228
test the gc queue is not empty while gil still unlocked
Aug 15, 2024
c995cb5
Merge remote-tracking branch 'origin/main' into v1
Sep 18, 2025
a996b4f
fixes after merge
Sep 18, 2025
090f05e
declare missing functions
Sep 18, 2025
06d3ad9
fix test for v1
Sep 18, 2025
f809e23
fix jl_help()
Sep 18, 2025
a29e591
not needed since v1 is a PR into main now
Sep 18, 2025
333b9a3
more REPL hacks
Sep 18, 2025
ae5a02d
Merge remote-tracking branch 'origin/main' into v1
Oct 23, 2025
59a16f9
comparisons return Bool not Py (#702)
cjdoris Oct 23, 2025
4b812e3
Delete unnecessary comparison operators (#698)
JamesWrigley Oct 23, 2025
d5f8ace
reparameterise PyArray (#674)
cjdoris Oct 23, 2025
03e2ed4
v1 migration guide (#704)
cjdoris Oct 23, 2025
20a60b8
Merge remote-tracking branch 'origin/main' into v1
Nov 1, 2025
d8fe069
Remove unused REPL compat
cjdoris Nov 2, 2025
c0d44b8
Remove Py-Number arithmetic methods (#709)
cjdoris Nov 2, 2025
d727e07
Merge remote-tracking branch 'origin/main' into v1
Nov 14, 2025
ab79641
fix tests (bad merge)
Nov 14, 2025
956cd60
Merge remote-tracking branch 'origin/main' into v1
Nov 18, 2025
2d0f36e
Merge remote-tracking branch 'origin/main' into v1
Jan 23, 2026
9263622
fix merge typo
Jan 23, 2026
9e2a551
fix missing import (merge error)
Jan 23, 2026
1ed4612
convert JuliaError.__str__ to python (#744)
dpinol Feb 15, 2026
0225c36
Merge remote-tracking branch 'origin/main' into v1
May 18, 2026
8f3b5ab
restore methods for !=, >= and > (#784)
cjdoris May 31, 2026
8161918
remove auto-sys-last-traceback functionality (#785)
cjdoris Jun 4, 2026
deb7570
remove unsafe API: getptr, pycopy!, pyisnull, pynew, PyNULL, unsafe_p…
cjdoris Jun 4, 2026
4d5dbb0
rename pydel! to unsafe_pydel and export (#788)
cjdoris Jun 4, 2026
bf1ff38
remove CONFIG, add fix_qt_plugin_path preference (#787)
cjdoris Jun 4, 2026
bcd976a
Merge remote-tracking branch 'origin/main' into v1
Jun 4, 2026
278cd38
fix ref in docs
Jun 4, 2026
0ec0320
Merge remote-tracking branch 'origin' into v1
Aug 29, 2026
edd64e2
Merge remote-tracking branch 'origin/main' into v1
Oct 19, 2026
90a03a1
Fix v1 CI after merging main
Oct 19, 2026
d007089
Merge remote-tracking branch 'origin/main' into v1
Oct 19, 2026
8cf2bfb
remove duplicated conda-build
Oct 19, 2026
5692bb7
Manage CPython thread states automatically across Julia tasks (#824)
cjdoris Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- v1
tags:
- '*'
pull_request:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- v1
push:
branches:
- main
Expand Down Expand Up @@ -63,7 +64,7 @@ jobs:
if: ${{ matrix.pythonexe == 'python' }}
with:
python-version: ${{ matrix.pyversion }}

- name: Build PyCall
if: ${{ matrix.pythonexe == 'python' }}
run: |
Expand Down
19 changes: 17 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
# Repository Instructions

## Running tests
- **Julia tests**: Run from the project root with `julia --project -e 'using Pkg; Pkg.test()'`. Expect a warning about the General registry being unreachable in locked-down environments; the suite still finishes (PyCall tests are marked broken/Skipped).
- **Julia tests**: Run from the project root with `julia --project -e 'using Pkg; Pkg.test()'`. Expect a warning about the General registry being unreachable in locked-down environments; PyCall tests are marked broken/skipped. With the currently installed Julia 1.13.1, the functional tests pass but Aqua's persistent-task subprocess check may fail because its `done.log` is not created.
- **Python tests**:
- Copy `pysrc/juliacall/juliapkg-dev.json` to `pysrc/juliacall/juliapkg.json` before running (do **not** commit this copy).
- Execute with `uv run pytest -s --nbval ./pytest` (add `--cov=pysrc` when coverage is needed).
- Sometimes `juliapkg` requires Julia 1.10–1.11; `juliaup` already provides 1.11.7 in this environment.
- Sometimes `juliapkg` requires Julia 1.10–1.11. Juliaup provides Julia 1.11.9
through the `1.11` channel. JuliaPkg selecting Julia 1.13.1 under
uv's downloaded Python 3.10 fails to load `libjulia-codegen` because the Python
process has loaded a `libstdc++.so.6` that lacks `GLIBCXX_3.4.30`. Set
`PYTHON_JULIAPKG_EXE` to the absolute path of the Julia 1.11 executable when
running this combination; `JULIAUP_CHANNEL=1.11` does not affect JuliaPkg's
executable discovery here. To test Julia 1.12 or 1.13 instead, also prepend the
repository's `.CondaPkg/.pixi/envs/default/lib` directory to `LD_LIBRARY_PATH` so
the process uses its newer `libstdc++`.
- `julia --project=docs docs/make.jl` requires a valid Git `origin` so Documenter can infer
source links; it fails during `makedocs` in checkouts without one.

The majority of tests live in the Julia package; Python tests cover functionality that cannot be exercised from Julia (e.g., JuliaCall-specific behavior). Run both suites—typically Julia first—in whichever order makes sense.

## Branch and CI compatibility
- On `v1`, JuliaCall uses `jl_eval` (returning a Julia wrapper), whereas `main` uses `seval`. Adapt tests brought over from `main` accordingly.
- The benchmark suite is shared across the compared revisions: `main` uses `pydel!`, while `v1` uses `unsafe_pydel`.
- `main` does not have `@pyregion`. Benchmark region wrappers must select a plain-body fallback before macro expansion; a runtime conditional around an unavailable macro still fails when loading the suite.
- Julia CI caches can preserve Conda's build metadata without its root environment. Rebuild `Conda` before building `PyCall` or running tests to recreate that directory.
- In restricted sandboxes, both suites need approval to write to the shared caches (`~/.julia` and `~/.cache/uv`).
- `pysrc/juliacall/juliapkg.json` is tracked; restore its original contents after using the development copy for Python tests.

Expand Down
18 changes: 9 additions & 9 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
| ------- | ------ | ------------------- | ----------- |
| Python | 1.0x | 280 | ? |
| PythonCall | 2.4x | 680 | 5008 |
| PythonCall + `pydel!` | 1.1x | 300 | 1008 |
| PythonCall + `unsafe_pydel` | 1.1x | 300 | 1008 |
| PythonCall `@py` | 1.4x | 420 | 1002 |
| PythonCall `@py` + `@pydel!` | 1.1x | 300 | 2 |
| PythonCall `@py` + `unsafe_pydel` | 1.1x | 300 | 2 |
| PyCall | 5.4x | 1620 | 10987 |
| PyCall (readable but wrong) | 5.9x | 1784 | 11456 |

Expand Down Expand Up @@ -42,7 +42,7 @@ test (generic function with 1 method)
julia> @benchmark test()
```

PythonCall + `pydel!` code:
PythonCall + `unsafe_pydel` code:
```julia-repl
julia> using PythonCall, BenchmarkTools

Expand All @@ -54,10 +54,10 @@ julia> function test()
r = random()
v = i + r
x[k] = v
pydel!(k)
pydel!(r)
pydel!(v)
pydel!(i)
unsafe_pydel(k)
unsafe_pydel(r)
unsafe_pydel(v)
unsafe_pydel(i)
end
return x
end
Expand All @@ -75,8 +75,8 @@ julia> test() = @py begin
x = {}
for i in range(1000)
x[str(i)] = i + random()
# Uncomment for pydel! version:
# @jl PythonCall.pydel!(i)
# Uncomment for unsafe_pydel version:
# @jl PythonCall.unsafe_pydel(i)
end
x
end
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# Changelog

## Unreleased (v1)
* The vast majority of these changes are breaking, see the [v1 Migration Guide](@ref) for how to upgrade.
* Changes to core functionality:
* Comparisons like `==`, `<` and `isless` between `Py`s now return `Bool` instead of `Py`.
* Removed comparisons between `Py` and `Number` (like `Py(3) < 5`).
* Removed arithmetic between `Py` and `Number` (like `Py(2) * 10`).
* Removed unsafe API: `getptr`, `pycopy!`, `pyisnull`, `pynew`, `PyNULL`, `unsafe_pynext`.
* Renamed `pydel!` to `unsafe_pydel` and exported.
A `Py` is now always a valid, fixed, non-NULL Python object in the documented API.
* Python errors no longer automatically set `sys.last_traceback` etc. when displayed from Julia.
* Added [`fix_qt_plugin_path` preference](@ref pythoncall-config), replacing `CONFIG.auto_fix_qt_plugin_path`.
* Removed `PythonCall.CONFIG`.
* Changes to multi-threading:
* PythonCall APIs now manage CPython thread states automatically and can be called from
any Julia task or thread without manually locking the GIL.
* Added the optional, nestable `@pyregion` and `@pyregionbreak` macros. These are
performance and concurrency hints rather than correctness requirements.
* Removed the `PythonCall.GIL` module. Ordinary PythonCall operations replace
`GIL.lock`, and `@pyregionbreak` replaces `GIL.unlock` when an enclosing region needs
to relinquish Python resources around yielding or blocking Julia code.
* JuliaCall now relinquishes and restores Python resources automatically while running
Julia code, including Python-to-Julia callbacks.
* Removed JuliaCall's `jl_call_nogil` method; ordinary calls now provide its behavior.
* Python-object finalizers no longer attach a Python thread state. Pending decrefs are
queued until a state is already attached or `PythonCall.GC.gc()` drains the queue.
* Changes to `PythonCall.GC` (now more like `Base.GC`):
* `enable(true)` replaces `enable()`.
* `enable(false)` replaces `disable()`.
* `gc()` added.
* Changes to Python wrapper types:
* `PyArray` has been reparametrised from `PyArray{T,N,M,L,R}` to `PyArray{T,N,F}`:
* `F` is a tuple of symbols representing flags, with `:linear` replacing `L` and `:mutable` replacing `M`.
* `R` is removed and is now implied by `T`, which currently must be either a bits type (equal to `R`) or `Py`, or a tuple of these.
* Changes to Julia wrapper types:
* Classes renamed: `ValueBase` to `JlBase`, `AnyValue` to `Jl`, `ArrayValue` to `JlArray`, etc.
* Classes removed: `RawValue`, `ModuleValue`, `TypeValue`, `NumberValue`, `ComplexValue`, `RealValue`, `RationalValue`, `IntegerValue`.
* `Jl` now behaves similar to how `RawValue` behaved before. In particular, most methods on `Jl` now return a `Jl` instead of an arbitrary Python object.
* `juliacall.Pkg` removed (you can import it yourself).
* `juliacall.convert` removed (use `juliacall.Jl` instead).
* Methods renamed: `_jl_display()` to `jl_display()`, `_jl_help()` to `jl_help()`, etc.
* Methods removed: `_jl_raw()`.
* `pyjl(x)` now always returns a `juliacall.Jl` (it used to select a wrapper type if possible).
* `pyjltype(x)` removed.
* New functions: `pyjlarray`, `pyjldict`, `pyjlset`.

## Unreleased
* Minimum supported Python version is now 3.11.

Expand Down
16 changes: 8 additions & 8 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39"

[weakdeps]
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"

[extensions]
CategoricalArraysExt = "CategoricalArrays"
PyCallExt = "PyCall"

[compat]
CategoricalArrays = "0.10, 1"
CondaPkg = "0.2.38"
Expand All @@ -28,17 +36,9 @@ Tables = "1"
UnsafePointers = "1"
julia = "1.10"

[extensions]
PyCallExt = "PyCall"
CategoricalArraysExt = "CategoricalArrays"

[extras]
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"

[weakdeps]
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"

[workspace]
projects = ["test", "docs", "benchmark"]
1 change: 1 addition & 0 deletions benchmark/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[deps]
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
67 changes: 61 additions & 6 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
using BenchmarkTools
using PythonCall
using PythonCall: pydel!, pyimport, pydict, pystr, pyrange
using PythonCall: pyimport, pydict, pystr, pyrange

# The same suite runs against main and v1, which use different names.
const benchmark_pydel = isdefined(PythonCall, :pydel!) ? PythonCall.pydel! : PythonCall.unsafe_pydel

# main has no regions: run the same body without the outer region there.
# Select before macro expansion so an unavailable @pyregion is never expanded.
macro benchmark_pyregion(ex)
if isdefined(PythonCall, Symbol("@pyregion"))
return esc(:(PythonCall.@pyregion $ex))
end
return esc(ex)
end

const SUITE = BenchmarkGroup()

Expand All @@ -23,10 +35,10 @@ function test_pydict_pydel()
r = random()
v = i + r
x[k] = v
pydel!(k)
pydel!(r)
pydel!(v)
pydel!(i)
benchmark_pydel(k)
benchmark_pydel(r)
benchmark_pydel(v)
benchmark_pydel(i)
end
return x
end
Expand All @@ -40,7 +52,7 @@ SUITE["basic"]["julia"]["pydict"]["pydel"] = @benchmarkable test_pydict_pydel()
x = {}
for i in range(1000)
x[str(i)] = i + random()
$(use_pydel ? :(@jl PythonCall.pydel!(i)) : :(nothing))
$(use_pydel ? :(@jl benchmark_pydel(i)) : :(nothing))
end
x
end
Expand All @@ -50,6 +62,49 @@ end
SUITE["basic"]["@py"]["pydict"]["init"] = @benchmarkable test_atpy(Val(false))
SUITE["basic"]["@py"]["pydict"]["pydel"] = @benchmarkable test_atpy(Val(true))

function test_region_pythoncall(x, n)
ans = 0
for _ = 1:n
ans += pylen(x)
end
return ans
end

function test_region_pythoncall_outer(x, n)
@benchmark_pyregion begin
ans = 0
for _ = 1:n
ans += pylen(x)
end
return ans
end
end

function test_region_capi(x, n)
@benchmark_pyregion begin
ans = 0
for _ = 1:n
ans += PythonCall.C.PyObject_Length(x)
end
return ans
end
end

const REGION_BENCHMARK_LENGTH = 1000

SUITE["region"]["pythoncall"] = @benchmarkable(
test_region_pythoncall(x, REGION_BENCHMARK_LENGTH),
setup=(x = pytuple((1, 2, 3))),
)
SUITE["region"]["pythoncall_outer"] = @benchmarkable(
test_region_pythoncall_outer(x, REGION_BENCHMARK_LENGTH),
setup=(x = pytuple((1, 2, 3))),
)
SUITE["region"]["capi_outer"] = @benchmarkable(
test_region_capi(x, REGION_BENCHMARK_LENGTH),
setup=(x = pytuple((1, 2, 3))),
)


include("gcbench.jl")
using .GCBench: append_lots
Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ makedocs(
"compat.md",
"faq.md",
"releasenotes.md",
"v1-migration-guide.md",
],
)

Expand Down
4 changes: 0 additions & 4 deletions docs/src/compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ Some packages require a little extra help to work nicely with PythonCall.
Some of these are "fixes" that are silently applied for you, and some are just extra
functions to bridge a gap. We aim to keep these as minimal as possible.

## Python standard library

Whenever a Python exception is displayed by Julia, `sys.last_traceback` and friends are set. This allows the post-mortem debugger `pdb.pm()` to work. Disable by setting `PythonCall.CONFIG.auto_sys_last_traceback = false`.

## Julia standard library

Python objects can be serialised with the [`Serialization`](https://docs.julialang.org/en/v1/stdlib/Serialization/) stdlib.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/conversion-to-julia.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ From Python, the arguments to a Julia function will be converted according to th
| From | To |
| :----------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| **Top priority (wrapped values).** | |
| `juliacall.AnyValue` | `Any` |
| `juliacall.Jl` | `Any` |
| **Very high priority (arrays).** | |
| Objects satisfying the buffer or array interface (inc. `bytes`, `bytearray`, `array.array`, `numpy.ndarray`) | `PyArray` |
| **High priority (canonical conversions).** | |
Expand Down
23 changes: 6 additions & 17 deletions docs/src/conversion-to-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ From Python, this occurs when converting the return value of a Julia function.
| `Dates.Time` | `datetime.time` |
| `Dates.DateTime` | `datetime.datetime` |
| `Dates.Second`, `Dates.Millisecond`, `Dates.Microsecond`, `Dates.Nanosecond` | `datetime.timedelta` |
| `Number` | `juliacall.NumberValue`, `juliacall.ComplexValue`, etc. |
| `AbstractArray` | `juliacall.ArrayValue`, `juliacall.VectorValue` |
| `AbstractDict` | `juliacall.DictValue` |
| `AbstractSet` | `juliacall.SetValue` |
| `IO` | `juliacall.BufferedIOValue` |
| `Module` | `juliacall.ModuleValue` |
| `Type` | `juliacall.TypeValue` |
| Anything else | `juliacall.AnyValue` |
| `AbstractArray` | `juliacall.JlArray`, `juliacall.JlVector` |
| `AbstractDict` | `juliacall.JlDict` |
| `AbstractSet` | `juliacall.JlSet` |
| `IO` | `juliacall.JlBinaryIO` |
| Anything else | `juliacall.Jl` |

See [here](@ref julia-wrappers) for an explanation of the `juliacall.*Value` wrapper types.
See [here](@ref julia-wrappers) for an explanation of the `juliacall.Jl*` wrapper types.

## [Custom rules](@id jl2py-conversion-custom)

Expand All @@ -47,11 +44,3 @@ object, then also define `ispy(::T) = true`.
```@docs
PythonCall.ispy
```

Alternatively, if you define a wrapper type (a subtype of
[`juliacall.AnyValue`](#juliacall.AnyValue)) then you may instead define `pyjltype(::T)` to
be that type.

```@docs
PythonCall.pyjltype
```
12 changes: 10 additions & 2 deletions docs/src/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Alternatively, to force PyCall to use the same interpreter as PythonCall, set th

## [Is PythonCall/JuliaCall thread safe?](@id faq-multi-threading)

Yes, as of v0.9.22, provided you handle the GIL correctly. See the guides for
Yes. PythonCall and JuliaCall manage the required Python thread states automatically, so
PythonCall operations can be used from any Julia task or thread without explicit GIL
handling. See the guides for
[PythonCall](@ref jl-multi-threading) and [JuliaCall](@ref py-multi-threading).

Before, tricks such as disabling the garbage collector were required. See the
Expand All @@ -30,7 +32,7 @@ Related issues:

## Issues when Numpy arrays are expected

When a Julia array is passed to Python, it is wrapped as a [`ArrayValue`](#juliacall.ArrayValue).
When a Julia array is passed to Python, it is wrapped as a [`JlArray`](#juliacall.JlArray).
This type satisfies the Numpy array interface and the buffer protocol, so can be used in
most places where a numpy array is valid.

Expand Down Expand Up @@ -116,3 +118,9 @@ Each release of PythonCall and JuliaCall will support and require:
- The current Julia LTS version and newer, [see here](https://julialang.org/downloads/#long_term_support_release). Currently 1.10+.

Only the latest patch release within each minor version is supported.

## Can I use the Python debugger `pdb` from Julia?

Yes! If you are used to using the post-mortem debugger `pdb.pm()` in Python, then you
can instead use `pdb.post_mortem(err[1].exception)` at the Julia REPL to debug the
most recent Python error (a [`PyException`](@ref)).
Loading
Loading