Calling Python from Julia
PythonCall.jl (the modern successor to the older PyCall.jl) lets Julia code import and call Python packages directly, converting between Julia and Python data types automatically at the boundary — a Julia Vector{Float64} becomes a NumPy array, a Julia Dict becomes a Python dict, and vice versa. This is invaluable for reusing mature Python libraries that have no Julia equivalent yet — calling matplotlib for a specific plot type, using pandas for a format Julia's DataFrames.jl doesn't yet parse, or wrapping a pretrained scikit-learn or transformers model — without leaving Julia or rewriting the Python logic; the underlying mechanism embeds a real Python interpreter in the Julia process via libpython, so calls have real interpreter overhead but no serialization cost since data is shared in memory.
Cricket analogy: It's like an IPL franchise fielding a marquee overseas signing (Steve Smith) rather than rebuilding his exact skillset from scratch domestically — PythonCall lets Julia 'sign' mature Python libraries like scikit-learn instead of reimplementing them.
using PythonCall
# Import a Python module and call it directly
np = pyimport("numpy")
plt = pyimport("matplotlib.pyplot")
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
plt.plot(x, y)
plt.savefig("sine_wave.png")
# Convert a Julia array to Python and back
julia_vec = [1.0, 2.0, 3.0]
py_arr = np.array(julia_vec) # Julia -> Python
back_to_julia = pyconvert(Vector{Float64}, py_arr) # Python -> JuliaPythonCall.jl manages its own isolated Conda/micromamba Python environment per Julia project by default (via CondaPkg.jl), so different Julia projects can depend on different, non-conflicting Python package versions without you managing virtualenvs manually.
Calling C and Fortran with ccall
Unlike calling Python, calling into a C shared library requires no wrapper package at all — ccall is a built-in Julia language construct that calls a C function directly with zero overhead, because Julia's own runtime is implemented largely in C and C already shares Julia's calling conventions at the machine level. The syntax ccall((:function_name, "libname"), ReturnType, (ArgType1, ArgType2), arg1, arg2) specifies the C function's symbol name, which shared library to load it from, its C return type, and a tuple of its C argument types, and Julia handles marshaling native Julia values (like Float64, Int32, or a Ptr{Float64} from an Array) into the exact bit representation the C ABI expects — no serialization, no interpreter, just a direct machine-code call.
Cricket analogy: It's like a direct, unbroken relay throw from deep cover straight into the keeper's gloves with no intermediate fielder — ccall makes a direct machine-code call to C with zero intermediate translation overhead, the way Ravindra Jadeja's flat, direct throws skip any relay stop.
Because ccall has no per-call overhead, it is the mechanism Julia's own standard library uses internally to wrap BLAS, LAPACK, and libc functions, and it's also how packages like Fortran90Wrappers.jl-style bindings call decades-old, battle-tested Fortran numerical routines directly — Fortran's bind(C) interoperability standard lets a Fortran subroutine expose a C-compatible ABI that ccall can then invoke exactly like any C function. The tradeoff is that ccall requires you to get argument types and memory layout exactly right — passing a Vector{Float64} where the C function expects a null-terminated Ptr{Cchar} string will not error at compile time, it will segfault or corrupt memory at runtime, since ccall bypasses Julia's normal type-safety checks by design.
Cricket analogy: It's like a fielder throwing directly at the stumps without going through the keeper — a perfectly aimed direct hit runs the batter out cleanly, but a slightly misjudged direct throw with no relay safety net can concede overthrows, just as a wrong ccall type has no safety net either.
ccall bypasses Julia's type-safety and garbage-collector guarantees. Passing incorrectly typed arguments, holding a Ptr past the lifetime of the Julia object it points to, or forgetting GC.@preserve around an array whose pointer you pass to a long-running C call are all common sources of memory corruption or segfaults that Julia's usual safety net does not catch.
Choosing Between the Two Approaches
In practice, reach for ccall when you need to call a compiled C or Fortran library directly for maximum performance and you're comfortable getting the low-level ABI details exactly right — this is how Julia itself talks to BLAS/LAPACK, and it's the right tool for wrapping a small, well-defined C API. Reach for PythonCall.jl when you want to reuse a large, complex Python library's actual logic (not just a thin C API) — machine learning frameworks, plotting libraries with rich stateful APIs, or anything where re-implementing the Python package in Julia (or writing a raw C binding to it) would be far more work than paying the modest interpreter-call overhead of talking to Python directly.
Cricket analogy: It's like choosing between fielding your own specialist death-over bowler (ccall — total control, needs precision) versus loaning a proven overseas all-rounder from another league for a season (PythonCall — reuse a complete, complex skillset without rebuilding it).
- PythonCall.jl (successor to PyCall.jl) embeds a real Python interpreter and auto-converts data between Julia and Python types at the call boundary.
- PythonCall manages an isolated Python environment per project via CondaPkg.jl, avoiding manual virtualenv management.
- ccall is a built-in Julia language construct for calling C/Fortran functions directly with zero overhead, no wrapper package required.
- ccall syntax specifies the C symbol name, library, return type, and argument types explicitly.
- Fortran routines can be called via ccall using Fortran's bind(C) interoperability standard to expose a C-compatible ABI.
- ccall bypasses Julia's type safety and GC guarantees; incorrect types or missing GC.@preserve can cause segfaults or memory corruption.
- Choose ccall for direct, high-performance calls to small well-defined C/Fortran APIs; choose PythonCall.jl to reuse large, complex Python libraries without reimplementing them.
Practice what you learned
1. What is the modern recommended package for calling Python from Julia, succeeding PyCall.jl?
2. What does `ccall` allow Julia code to do?
3. Why does calling a Fortran subroutine via ccall require the Fortran code to use bind(C)?
4. What is a key risk of using ccall incorrectly?
5. When is PythonCall.jl generally the better choice over writing a ccall-based binding?
Was this page helpful?
You May Also Like
Julia for Scientific Computing
How Julia's multiple dispatch, type system, and native array performance make it a first-class language for numerical and scientific computing.
Julia vs Python
A practical comparison of Julia and Python for numerical computing, covering performance, syntax, and ecosystem tradeoffs.
The Julia Package Manager (Pkg)
How to use Julia's built-in Pkg to create reproducible environments, add and version packages, and understand Project.toml and Manifest.toml.
Julia and Machine Learning (Flux.jl)
How Flux.jl brings differentiable programming to Julia, letting you build and train neural networks with plain Julia code and automatic differentiation.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics