PythonCall.jl Documentation

repository·main·Indexed 21 days ago

https://github.com/juliapy/pythoncall.jl

A symmetric, high-performance interface for seamless interoperability between Python and Julia. PythonCall.jl allows calling Python from Julia, while the companion JuliaCall module enables calling Julia from Python. Key features include non-copying data conversions, the @py macro for native Python code in Julia, and integration with CondaPkg.jl for isolated dependency management. It provides specialized tools for converting Pandas DataFrames to Julia Tables, managing Python object lifecycles with pydel!, and an IPython extension for interactive sessions.

Tokens
14.9K
Snippets
57
Records
74
Agent score
74%

What's inside PythonCall.jl

  1. Overview of PythonCall and JuliaCall

    main

    PythonCall and JuliaCall provide a symmetric interface for seamless interoperability between Python and Julia.

    Key features include:

    • Symmetric Interface: Call Python code from Julia and Julia code from Python using syntax that feels native to each language.
    • Flexible Conversions: Intuitive conversion between Julia and Python types where the user maintains control.
    • High-Performance Numeric Arrays: Fast, non-copying conversion for numeric arrays (e.g., numpy.ndarray, bytes, array.array in Python, or Julia arrays), allowing for in-place modifications across the language boundary.
    • Helpful Wrappers: Automatic interpretation of Python sequences, dictionaries, arrays, dataframes, and IO streams as their Julia counterparts, and vice versa.
    • Cross-Platform Support: Tested on Windows, MacOS, and Linux (64-bit).
    • Compatibility: Requires Julia 1.10+ and Python 3.10+.
  2. Understand Julia to Python conversion rules

    main

    When passing Julia objects to Python functions or using the explicit Py(x) constructor, PythonCall automatically converts Julia types to their Python equivalents.

    Conversion Triggers:

    • Explicit: Calling Py(x).
    • Implicit: Passing a Julia object as an argument to a Python function.
    • From Python: When a Julia function returns a value to Python.

    To prevent automatic conversion and keep the object in its Julia form, use explicit conversion methods like pylist or pydict (depending on the desired structure) or wrap it in a way that avoids the default mapping.

  3. Key differences between PythonCall and PyCall

    main

    If you are deciding between PythonCall.jl and the existing PyCall.jl, consider these architectural differences:

    • Conversion & Extensibility: PythonCall supports a wider, more extensible range of conversions.
    • Memory Management: PythonCall avoids copying mutable objects by default, instead wrapping them. Modifying a converted object modifies the original, which is faster and more memory-efficient.
    • Type Stability & Pythonic Behavior: PythonCall typically leaves results as Python objects rather than automatically converting them to Julia values. This allows for easier method access on Python objects and better type stability.
    • Dependency Management: PythonCall uses CondaPkg.jl to install dependencies into isolated Conda environments specific to each Julia project, preventing dependency conflicts between different projects.
  4. How Julia values are wrapped in Python

    main

    Most Julia values are automatically converted into Python objects that wrap the original Julia value. These wrappers provide a Pythonic interface while maintaining a connection to the underlying Julia data.

    • AnyValue: The base wrapper for most Julia objects. It supports standard Python protocols like len(), iteration, comparisons, and attribute access. Attribute access can also access Julia properties. To handle Julia's mutation syntax, _b at the end of an attribute is converted to ! and _bb to !!.
    • RawValue: A stricter wrapper designed for generic programming. Unlike AnyValue, operations like indexing or calling on a RawValue always return another RawValue rather than converting to a Python type.
    • ValueBase: The common parent class for all wrapper types.
  5. How Python wrapper types work

    main

    When converting from Python to Julia, PythonCall often returns 'wrapper types' instead of raw Py objects. These wrappers provide Julia-native semantics for Python objects. For example, a PyList behaves like a Julia abstract vector.

    # Common wrapper types returned during conversion:
    # PyList, PySet, PyDict, PyIterable, PyArray, PyIO, PyTable, PyPandasDataFrame, PyObjectArray, PyException
  6. Handle multi-threading with PythonCall and JuliaCall

    main

    As of v0.9.22, PythonCall and JuliaCall are thread-safe provided you handle the Global Interpreter Lock (GIL) correctly.

    When starting a Julia REPL with multiple threads, you must ensure there is exactly one interactive thread to avoid segmentation faults during tab completion. You can configure this using the JULIA_NUM_THREADS environment variable in the format X,1 (where X is the number of worker threads) or via the --threads CLI flag.

    julia --threads X,1
  7. Convert between Julia and Python

    main

    PythonCall handles conversions in two ways:

    1. Implicit Conversion: When passing Julia objects as arguments to Python functions, they are automatically converted to Python objects.
    2. Explicit Conversion:
      • Use Py(x) to wrap a Julia object as a Py object.
      • Use pyconvert(T, x) to convert a Python object x into a specific Julia type T.

    Note on pyconvert(Any, x):

    • For immutable scalars (like int or str), it returns the corresponding Julia object.
    • For containers, it returns a wrapper type (like PyList{Py}) which is a no-copy view. Mutating the wrapper mutates the original Python object.
    # Convert Python list to Julia Vector
    x = pylist([3.4, 5.6])
    
    # Specific type
    vec = pyconvert(Vector{Float64}, x)
    
    # No-copy wrapper (mutating 'any_vec' mutates 'x')
    any_vec = pyconvert(Any, x)
  8. Perform arithmetic and logic with Py objects

    main

    PythonCall overloads Julia's standard arithmetic and logical operators to work with Py objects. This means you can use standard Julia syntax (e.g., +, -, *, <, ==) when interacting with Python objects, and it will correctly invoke the underlying Python operations.

    # These are all equivalent:
    # 1. Using overloaded Julia operators
    result = Py(1) + Py(2)
    
    # 2. Mixing Py objects and Julia numbers
    result = Py(1) + 2
    
    # 3. Using explicit PythonCall arithmetic functions
    result = pyadd(Py(1), Py(2))
    
    # Logic follows the same pattern:
    # Py(1) < Py(2) is equivalent to pylt(Py(1), Py(2))
  9. Use multi-threading with _jl_call_nogil()

    main

    To achieve true parallelism in Python threads when calling Julia, you must unlock the Python Global Interpreter Lock (GIL).

    How to use: Use the _jl_call_nogil method on a Julia function. This allows the thread to run Julia code without holding the GIL, enabling other Python threads to run simultaneously.

    Warning:

    • Any function called with _jl_call_nogil must not interact with Python unless it re-locks the GIL (e.g., using PythonCall.GIL.@lock).
    • If the Julia function yields to the task scheduler (like sleep), you may experience a hang. In such cases, you must periodically call jl.yield() from Python to allow the Julia scheduler to cycle.
    • Signal Handling: When using multiple threads, it is highly recommended to set PYTHON_JULIACALL_HANDLE_SIGNALS=yes to prevent segmentation faults caused by Julia's GC safepoint mechanism. Note that this may interfere with Python's signal handling (e.g., Ctrl-C might not work).
    from concurrent.futures import ThreadPoolExecutor, wait
    from juliacall import Main as jl
    
    pool = ThreadPoolExecutor(4)
    # Use _jl_call_nogil to allow parallel execution
    fs = [pool.submit(jl.Libc.systemsleep._jl_call_nogil, 5) for _ in range(4)]
    wait(fs)
  10. Use special values for the `exe` configuration

    main

    The exe preference (or JULIA_PYTHONCALL_EXE environment variable) allows you to point to specific Python environments using special tokens:

    • @CondaPkg: Use Python from CondaPkg (the default).
    • @PyCall: Use the same Python as PyCall.
    • @venv: Use Python from a .venv virtual environment in the current active project.

    Otherwise, the value is interpreted as an absolute path, a relative path (resolved relative to the current active project), or a command name to search for in PATH.

  11. Use PythonCall wrapper types

    main

    Wrapper types allow you to use Python objects with Julia semantics without copying the underlying data. Mutating a wrapper mutates the original Python object.

    Common wrappers include:

    • PyList{T}: Wraps Python sequences (lists, etc.) as a Julia AbstractVector.
    • PyDict{K, V}: Wraps Python dictionaries.
    • PySet{T}: Wraps Python sets.
    • PyArray: Provides a high-performance Julia array view of Python arrays (e.g., numpy.ndarray or array.array) via the buffer protocol.
    • PyIO: Wraps a Python file-like object as a Julia IO object.
    # PyArray example
    x = pyimport("array").array("i", [3, 4, 5])
    y = PyArray(x)
    sum(y) # 12
    
    # PyIO example
    x = pyimport("io").StringIO()
    y = PyIO(x)
    println(y, "Hello, world!")
    flush(y)
  12. Install juliacall

    main

    You can install the juliacall module using pip or conda.

    Using pip:

    pip install juliacall

    Using Conda:

    conda install conda-forge::pyjuliacall

    For Developers: If you are developing PythonCall.jl and want to use it in editable mode with juliacall, add "dev":true, "path":"../.." to pysrc/juliacall/juliapkg.json to ensure the development version is used.