Skip to main content

polars_python/lazyframe/visitor/
expr_nodes.rs

1#[cfg(feature = "iejoin")]
2use polars::prelude::InequalityOperator;
3use polars::series::ops::NullBehavior;
4use polars_compute::rolling::{QuantileMethod, RollingFnParams};
5use polars_core::chunked_array::ops::FillNullStrategy;
6#[cfg(feature = "string_normalize")]
7use polars_ops::chunked_array::UnicodeForm;
8use polars_ops::prelude::RankMethod;
9#[cfg(feature = "search_sorted")]
10use polars_ops::series::SearchSortedSide;
11use polars_ops::series::{ClosedInterval, InterpolationMethod};
12use polars_plan::dsl::DateRangeArgs;
13use polars_plan::plans::{
14    DynListLiteralValue, DynLiteralValue, FusedOperator, IRArrayFunction, IRBitwiseFunction,
15    IRBooleanFunction, IRCorrelationMethod, IRFunctionExpr, IRListFunction, IRPowFunction,
16    IRRandomMethod, IRRangeFunction, IRRollingFunction, IRRollingFunctionBy, IRStringFunction,
17    IRStructFunction, IRTemporalFunction,
18};
19use polars_plan::prelude::{
20    AExpr, GroupbyOptions, IRAggExpr, LiteralValue, Operator, PlanCallback, WindowMapping,
21};
22use polars_time::prelude::RollingGroupOptions;
23use polars_time::{ClosedWindow, Duration, DynamicGroupOptions};
24use polars_utils::itertools::Itertools;
25use pyo3::IntoPyObjectExt;
26use pyo3::exceptions::PyNotImplementedError;
27use pyo3::prelude::*;
28use pyo3::types::{PyBytes, PyInt, PyList, PyTuple};
29
30use crate::Wrap;
31use crate::lazyframe::visit::PyExprIR;
32use crate::series::PySeries;
33
34#[pyclass(frozen)]
35pub struct Alias {
36    #[pyo3(get)]
37    expr: usize,
38    #[pyo3(get)]
39    name: Py<PyAny>,
40}
41
42#[pyclass(frozen)]
43pub struct Column {
44    #[pyo3(get)]
45    name: Py<PyAny>,
46}
47
48#[pyclass(frozen)]
49pub struct Literal {
50    #[pyo3(get)]
51    value: Py<PyAny>,
52    #[pyo3(get)]
53    dtype: Py<PyAny>,
54}
55
56#[pyclass(name = "Operator", eq, frozen, skip_from_py_object)]
57#[derive(Copy, Clone, PartialEq)]
58pub enum PyOperator {
59    Eq,
60    EqValidity,
61    NotEq,
62    NotEqValidity,
63    Lt,
64    LtEq,
65    Gt,
66    GtEq,
67    Plus,
68    Minus,
69    Multiply,
70    Divide,
71    TrueDivide,
72    FloorDivide,
73    Modulus,
74    And,
75    Or,
76    Xor,
77    LogicalAnd,
78    LogicalOr,
79}
80
81#[pymethods]
82impl PyOperator {
83    fn __hash__(&self) -> isize {
84        *self as isize
85    }
86}
87
88impl<'py> IntoPyObject<'py> for Wrap<Operator> {
89    type Target = PyOperator;
90    type Output = Bound<'py, Self::Target>;
91    type Error = PyErr;
92
93    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
94        match self.0 {
95            Operator::Eq => PyOperator::Eq,
96            Operator::EqValidity => PyOperator::EqValidity,
97            Operator::NotEq => PyOperator::NotEq,
98            Operator::NotEqValidity => PyOperator::NotEqValidity,
99            Operator::Lt => PyOperator::Lt,
100            Operator::LtEq => PyOperator::LtEq,
101            Operator::Gt => PyOperator::Gt,
102            Operator::GtEq => PyOperator::GtEq,
103            Operator::Plus => PyOperator::Plus,
104            Operator::Minus => PyOperator::Minus,
105            Operator::Multiply => PyOperator::Multiply,
106            Operator::RustDivide => PyOperator::Divide,
107            Operator::TrueDivide => PyOperator::TrueDivide,
108            Operator::FloorDivide => PyOperator::FloorDivide,
109            Operator::Modulus => PyOperator::Modulus,
110            Operator::And => PyOperator::And,
111            Operator::Or => PyOperator::Or,
112            Operator::Xor => PyOperator::Xor,
113            Operator::LogicalAnd => PyOperator::LogicalAnd,
114            Operator::LogicalOr => PyOperator::LogicalOr,
115        }
116        .into_pyobject(py)
117    }
118}
119
120#[cfg(feature = "iejoin")]
121impl<'py> IntoPyObject<'py> for Wrap<InequalityOperator> {
122    type Target = PyOperator;
123    type Output = Bound<'py, Self::Target>;
124    type Error = PyErr;
125
126    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
127        match self.0 {
128            InequalityOperator::Lt => PyOperator::Lt,
129            InequalityOperator::LtEq => PyOperator::LtEq,
130            InequalityOperator::Gt => PyOperator::Gt,
131            InequalityOperator::GtEq => PyOperator::GtEq,
132        }
133        .into_pyobject(py)
134    }
135}
136
137#[pyclass(name = "StringFunction", eq, frozen, skip_from_py_object)]
138#[derive(Copy, Clone, PartialEq)]
139pub enum PyStringFunction {
140    ConcatHorizontal,
141    ConcatVertical,
142    Contains,
143    CountMatches,
144    EndsWith,
145    Extract,
146    ExtractAll,
147    ExtractGroups,
148    Find,
149    ToInteger,
150    LenBytes,
151    LenChars,
152    Lowercase,
153    JsonDecode,
154    JsonPathMatch,
155    Replace,
156    Reverse,
157    PadStart,
158    PadEnd,
159    Slice,
160    Head,
161    Tail,
162    HexEncode,
163    HexDecode,
164    Base64Encode,
165    Base64Decode,
166    StartsWith,
167    StripChars,
168    StripCharsStart,
169    StripCharsEnd,
170    StripPrefix,
171    StripSuffix,
172    SplitExact,
173    SplitN,
174    Strptime,
175    Split,
176    SplitRegex,
177    ToDecimal,
178    Titlecase,
179    Uppercase,
180    ZFill,
181    ContainsAny,
182    ReplaceMany,
183    EscapeRegex,
184    Normalize,
185    Format,
186    ExtractMany,
187    FindMany,
188}
189
190#[pymethods]
191impl PyStringFunction {
192    fn __hash__(&self) -> isize {
193        *self as isize
194    }
195}
196
197#[pyclass(name = "BooleanFunction", eq, frozen, skip_from_py_object)]
198#[derive(Copy, Clone, PartialEq)]
199pub enum PyBooleanFunction {
200    Any,
201    All,
202    IsEmpty,
203    HasNulls,
204    IsNull,
205    IsNotNull,
206    IsFinite,
207    IsInfinite,
208    IsNan,
209    IsNotNan,
210    IsFirstDistinct,
211    IsLastDistinct,
212    IsUnique,
213    IsDuplicated,
214    IsBetween,
215    IsIn,
216    IsClose,
217    IsSorted,
218    AllHorizontal,
219    AnyHorizontal,
220    Not,
221}
222
223#[pymethods]
224impl PyBooleanFunction {
225    fn __hash__(&self) -> isize {
226        *self as isize
227    }
228}
229
230#[pyclass(name = "TemporalFunction", eq, frozen, skip_from_py_object)]
231#[derive(Copy, Clone, PartialEq)]
232pub enum PyTemporalFunction {
233    Millennium,
234    Century,
235    Year,
236    IsLeapYear,
237    IsoYear,
238    Quarter,
239    Month,
240    DaysInMonth,
241    Week,
242    WeekDay,
243    Day,
244    OrdinalDay,
245    Time,
246    Date,
247    Datetime,
248    Duration,
249    Hour,
250    Minute,
251    Second,
252    Millisecond,
253    Microsecond,
254    Nanosecond,
255    TotalDays,
256    TotalHours,
257    TotalMinutes,
258    TotalSeconds,
259    TotalMilliseconds,
260    TotalMicroseconds,
261    TotalNanoseconds,
262    ToString,
263    CastTimeUnit,
264    WithTimeUnit,
265    ConvertTimeZone,
266    TimeStamp,
267    Truncate,
268    OffsetBy,
269    MonthStart,
270    MonthEnd,
271    BaseUtcOffset,
272    DSTOffset,
273    Round,
274    Replace,
275    ReplaceTimeZone,
276    Combine,
277    DatetimeFunction,
278}
279
280#[pymethods]
281impl PyTemporalFunction {
282    fn __hash__(&self) -> isize {
283        *self as isize
284    }
285}
286
287#[pyclass(name = "StructFunction", eq, frozen, skip_from_py_object)]
288#[derive(Copy, Clone, PartialEq)]
289pub enum PyStructFunction {
290    FieldByName,
291    RenameFields,
292    DropFields,
293    PrefixFields,
294    SuffixFields,
295    JsonEncode,
296    WithFields,
297    MapFieldNames,
298}
299
300#[pymethods]
301impl PyStructFunction {
302    fn __hash__(&self) -> isize {
303        *self as isize
304    }
305}
306
307#[pyclass(name = "ListFunction", eq, frozen, skip_from_py_object)]
308#[derive(Copy, Clone, PartialEq)]
309pub enum PyListFunction {
310    Concat,
311    Contains,
312    DropNulls,
313    Get,
314    Length,
315    Sort,
316    SetOperation,
317    Sample,
318    Slice,
319    Shift,
320    Gather,
321    GatherEvery,
322    CountMatches,
323    Sum,
324    Max,
325    Min,
326    Mean,
327    Median,
328    Std,
329    Var,
330    ArgMin,
331    ArgMax,
332    Diff,
333    Join,
334    ToArray,
335    ToStruct,
336}
337
338#[pymethods]
339impl PyListFunction {
340    fn __hash__(&self) -> isize {
341        *self as isize
342    }
343}
344
345#[pyclass(name = "ArrayFunction", eq, frozen, skip_from_py_object)]
346#[derive(Copy, Clone, PartialEq)]
347pub enum PyArrayFunction {
348    Length,
349    Min,
350    Max,
351    Sum,
352    ToList,
353    Std,
354    Var,
355    Mean,
356    Median,
357    Sort,
358    ArgMin,
359    ArgMax,
360    Get,
361    Join,
362    Contains,
363    CountMatches,
364    Shift,
365    Explode,
366    Concat,
367    Slice,
368    ToStruct,
369}
370
371#[pymethods]
372impl PyArrayFunction {
373    fn __hash__(&self) -> isize {
374        *self as isize
375    }
376}
377
378#[pyclass(name = "RollingFunction", eq, frozen, skip_from_py_object)]
379#[derive(Copy, Clone, PartialEq)]
380pub enum PyRollingFunction {
381    Min,
382    Max,
383    Mean,
384    Sum,
385    Quantile,
386    Var,
387    Std,
388    Rank,
389    Skew,
390    Kurtosis,
391    CorrCov,
392}
393
394#[pymethods]
395impl PyRollingFunction {
396    fn __hash__(&self) -> isize {
397        *self as isize
398    }
399}
400
401#[pyclass(name = "EwmFunction", eq, frozen, skip_from_py_object)]
402#[derive(Copy, Clone, PartialEq)]
403pub enum PyEwmFunction {
404    Mean,
405    Sum,
406    Std,
407    Var,
408    MeanBy,
409    SumBy,
410}
411
412#[pymethods]
413impl PyEwmFunction {
414    fn __hash__(&self) -> isize {
415        *self as isize
416    }
417}
418
419#[pyclass(name = "RollingFunctionBy", eq, frozen, skip_from_py_object)]
420#[derive(Copy, Clone, PartialEq)]
421pub enum PyRollingFunctionBy {
422    MinBy,
423    MaxBy,
424    MeanBy,
425    SumBy,
426    QuantileBy,
427    VarBy,
428    StdBy,
429    RankBy,
430}
431
432#[pymethods]
433impl PyRollingFunctionBy {
434    fn __hash__(&self) -> isize {
435        *self as isize
436    }
437}
438
439#[pyclass(name = "BitwiseFunction", eq, frozen, skip_from_py_object)]
440#[derive(Copy, Clone, PartialEq)]
441pub enum PyBitwiseFunction {
442    CountOnes,
443    CountZeros,
444    LeadingOnes,
445    LeadingZeros,
446    TrailingOnes,
447    TrailingZeros,
448    And,
449    Or,
450    Xor,
451}
452
453#[pymethods]
454impl PyBitwiseFunction {
455    fn __hash__(&self) -> isize {
456        *self as isize
457    }
458}
459
460#[pyclass(name = "RangeFunction", eq, frozen, skip_from_py_object)]
461#[derive(Copy, Clone, PartialEq)]
462pub enum PyRangeFunction {
463    IntRange,
464    IntRanges,
465    LinearSpace,
466    LinearSpaces,
467    DateRange,
468    DateRanges,
469    DatetimeRange,
470    DatetimeRanges,
471    TimeRange,
472    TimeRanges,
473}
474
475#[pymethods]
476impl PyRangeFunction {
477    fn __hash__(&self) -> isize {
478        *self as isize
479    }
480}
481
482#[pyclass(frozen)]
483pub struct BinaryExpr {
484    #[pyo3(get)]
485    left: usize,
486    #[pyo3(get)]
487    op: Py<PyAny>,
488    #[pyo3(get)]
489    right: usize,
490}
491
492#[pyclass(frozen)]
493pub struct Cast {
494    #[pyo3(get)]
495    expr: usize,
496    #[pyo3(get)]
497    dtype: Py<PyAny>,
498    // 0: strict
499    // 1: non-strict
500    // 2: overflow
501    #[pyo3(get)]
502    options: u8,
503}
504
505#[pyclass(frozen)]
506pub struct Sort {
507    #[pyo3(get)]
508    expr: usize,
509    #[pyo3(get)]
510    /// maintain_order, nulls_last, descending
511    options: (bool, bool, bool),
512}
513
514#[pyclass(frozen)]
515pub struct Gather {
516    #[pyo3(get)]
517    expr: usize,
518    #[pyo3(get)]
519    idx: usize,
520    #[pyo3(get)]
521    scalar: bool,
522}
523
524#[pyclass(frozen)]
525pub struct Filter {
526    #[pyo3(get)]
527    input: usize,
528    #[pyo3(get)]
529    by: usize,
530}
531
532#[pyclass(frozen)]
533pub struct SortBy {
534    #[pyo3(get)]
535    expr: usize,
536    #[pyo3(get)]
537    by: Vec<usize>,
538    #[pyo3(get)]
539    /// maintain_order, nulls_last, descending
540    sort_options: (bool, Vec<bool>, Vec<bool>),
541}
542
543#[pyclass(frozen)]
544pub struct Agg {
545    #[pyo3(get)]
546    name: Py<PyAny>,
547    #[pyo3(get)]
548    arguments: Vec<usize>,
549    #[pyo3(get)]
550    // Arbitrary control options
551    options: Py<PyAny>,
552}
553
554#[pyclass(frozen)]
555pub struct Ternary {
556    #[pyo3(get)]
557    predicate: usize,
558    #[pyo3(get)]
559    truthy: usize,
560    #[pyo3(get)]
561    falsy: usize,
562}
563
564#[pyclass(frozen)]
565pub struct Function {
566    #[pyo3(get)]
567    input: Vec<usize>,
568    #[pyo3(get)]
569    function_data: Py<PyAny>,
570    #[pyo3(get)]
571    options: Py<PyAny>,
572}
573
574#[pyclass(frozen)]
575pub struct Slice {
576    #[pyo3(get)]
577    input: usize,
578    #[pyo3(get)]
579    offset: usize,
580    #[pyo3(get)]
581    length: usize,
582}
583
584#[pyclass(frozen)]
585pub struct Len {}
586
587#[pyclass(frozen)]
588pub struct StructEval {
589    #[pyo3(get)]
590    expr: usize,
591    #[pyo3(get)]
592    evaluation: Vec<PyExprIR>,
593}
594
595#[pyclass(frozen)]
596pub struct Explode {
597    #[pyo3(get)]
598    expr: usize,
599    #[pyo3(get)]
600    // empty_as_null, keep_nulls
601    options: (bool, bool),
602}
603
604#[pyclass(frozen)]
605pub struct Window {
606    #[pyo3(get)]
607    function: usize,
608    #[pyo3(get)]
609    partition_by: Vec<usize>,
610    #[pyo3(get)]
611    order_by: Option<usize>,
612    #[pyo3(get)]
613    order_by_descending: bool,
614    #[pyo3(get)]
615    order_by_nulls_last: bool,
616    #[pyo3(get)]
617    options: Py<PyAny>,
618}
619
620#[pyclass(frozen)]
621pub struct Rolling {
622    #[pyo3(get)]
623    function: usize,
624    #[pyo3(get)]
625    index_column: usize,
626    #[pyo3(get)]
627    period: Py<PyAny>,
628    #[pyo3(get)]
629    offset: Py<PyAny>,
630    #[pyo3(get)]
631    closed_window: Py<PyAny>,
632}
633
634#[pyclass(name = "WindowMapping", frozen)]
635pub struct PyWindowMapping {
636    inner: WindowMapping,
637}
638
639#[pymethods]
640impl PyWindowMapping {
641    #[getter]
642    fn kind(&self) -> &str {
643        self.inner.into()
644    }
645}
646
647impl<'py> IntoPyObject<'py> for Wrap<Duration> {
648    type Target = PyTuple;
649    type Output = Bound<'py, Self::Target>;
650    type Error = PyErr;
651
652    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
653        (
654            self.0.months(),
655            self.0.weeks(),
656            self.0.days(),
657            self.0.nanoseconds(),
658            self.0.parsed_int,
659            self.0.negative(),
660        )
661            .into_pyobject(py)
662    }
663}
664
665impl<'py> IntoPyObject<'py> for Wrap<ClosedWindow> {
666    type Target = PyAny;
667    type Output = Bound<'py, Self::Target>;
668    type Error = PyErr;
669
670    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
671        let s = match self.0 {
672            ClosedWindow::Left => "left",
673            ClosedWindow::Right => "right",
674            ClosedWindow::Both => "both",
675            ClosedWindow::None => "none",
676        };
677        Ok(s.into_pyobject(py)?.into_any())
678    }
679}
680
681#[pyclass(name = "RollingGroupOptions", frozen)]
682pub struct PyRollingGroupOptions {
683    inner: RollingGroupOptions,
684}
685
686#[pymethods]
687impl PyRollingGroupOptions {
688    #[getter]
689    fn index_column(&self) -> &str {
690        self.inner.index_column.as_str()
691    }
692
693    #[getter]
694    fn period(&self) -> Wrap<Duration> {
695        Wrap(self.inner.period)
696    }
697
698    #[getter]
699    fn offset(&self) -> Wrap<Duration> {
700        Wrap(self.inner.offset)
701    }
702
703    #[getter]
704    fn closed_window(&self) -> &str {
705        self.inner.closed_window.into()
706    }
707}
708
709#[pyclass(name = "DynamicGroupOptions", frozen)]
710pub struct PyDynamicGroupOptions {
711    inner: DynamicGroupOptions,
712}
713
714#[pymethods]
715impl PyDynamicGroupOptions {
716    #[getter]
717    fn index_column(&self) -> &str {
718        self.inner.index_column.as_str()
719    }
720
721    #[getter]
722    fn every(&self) -> Wrap<Duration> {
723        Wrap(self.inner.every)
724    }
725
726    #[getter]
727    fn period(&self) -> Wrap<Duration> {
728        Wrap(self.inner.period)
729    }
730
731    #[getter]
732    fn offset(&self) -> Wrap<Duration> {
733        Wrap(self.inner.offset)
734    }
735
736    #[getter]
737    fn label(&self) -> &str {
738        self.inner.label.into()
739    }
740
741    #[getter]
742    fn include_boundaries(&self) -> bool {
743        self.inner.include_boundaries
744    }
745
746    #[getter]
747    fn closed_window(&self) -> &str {
748        self.inner.closed_window.into()
749    }
750    #[getter]
751    fn start_by(&self) -> &str {
752        self.inner.start_by.into()
753    }
754}
755
756#[pyclass(name = "GroupbyOptions", frozen)]
757pub struct PyGroupbyOptions {
758    inner: GroupbyOptions,
759}
760
761impl PyGroupbyOptions {
762    pub(crate) fn new(inner: GroupbyOptions) -> Self {
763        Self { inner }
764    }
765}
766
767#[pymethods]
768impl PyGroupbyOptions {
769    #[getter]
770    fn slice(&self) -> Option<(i64, usize)> {
771        self.inner.slice
772    }
773
774    #[getter]
775    fn dynamic(&self) -> Option<PyDynamicGroupOptions> {
776        self.inner
777            .dynamic
778            .as_ref()
779            .map(|f| PyDynamicGroupOptions { inner: f.clone() })
780    }
781
782    #[getter]
783    fn rolling(&self) -> Option<PyRollingGroupOptions> {
784        self.inner
785            .rolling
786            .as_ref()
787            .map(|f| PyRollingGroupOptions { inner: f.clone() })
788    }
789}
790
791// Serialize the optional per-op rolling parameters into the flat tuple consumed by
792// external engines (e.g., cudf-polars).
793fn rolling_fn_params_into_py(
794    py: Python<'_>,
795    fn_params: &Option<RollingFnParams>,
796) -> PyResult<Py<PyAny>> {
797    match fn_params {
798        None => ().into_py_any(py),
799        Some(RollingFnParams::Quantile(q)) => {
800            (q.prob, Into::<&str>::into(q.method)).into_py_any(py)
801        },
802        Some(RollingFnParams::Var(v)) => (v.ddof,).into_py_any(py),
803        Some(RollingFnParams::Rank { method, seed }) => {
804            let method = Into::<&str>::into(method);
805            (method, *seed).into_py_any(py)
806        },
807        Some(RollingFnParams::Skew { bias }) => (*bias,).into_py_any(py),
808        Some(RollingFnParams::Kurtosis { fisher, bias }) => (*fisher, *bias).into_py_any(py),
809    }
810}
811
812fn closed_interval_into_py(closed: &ClosedInterval) -> &'static str {
813    match closed {
814        ClosedInterval::Both => "both",
815        ClosedInterval::Left => "left",
816        ClosedInterval::Right => "right",
817        ClosedInterval::None => "none",
818    }
819}
820
821fn date_range_args_into_py(arg_type: &DateRangeArgs) -> &'static str {
822    match arg_type {
823        DateRangeArgs::StartEndInterval => "start_end_interval",
824        DateRangeArgs::StartEndSamples => "start_end_samples",
825        DateRangeArgs::StartIntervalSamples => "start_interval_samples",
826        DateRangeArgs::EndIntervalSamples => "end_interval_samples",
827    }
828}
829
830// Recursively convert a nested dynamic list literal into Python lists.
831fn dyn_list_literal_into_py(py: Python<'_>, value: &DynListLiteralValue) -> PyResult<Py<PyAny>> {
832    match value {
833        DynListLiteralValue::Str(values) => values
834            .iter()
835            .map(|v| v.as_ref().map(|s| s.as_str()))
836            .collect::<Vec<_>>()
837            .into_py_any(py),
838        DynListLiteralValue::Int(values) => values.to_vec().into_py_any(py),
839        DynListLiteralValue::Float(values) => values.to_vec().into_py_any(py),
840        DynListLiteralValue::List(values) => {
841            let out = PyList::new(py, [] as [Bound<'_, PyAny>; 0])?;
842            for v in values.iter() {
843                match v {
844                    None => out.append(py.None())?,
845                    Some(inner) => out.append(dyn_list_literal_into_py(py, inner)?)?,
846                }
847            }
848            out.into_py_any(py)
849        },
850    }
851}
852
853pub(crate) fn into_py(py: Python<'_>, expr: &AExpr) -> PyResult<Py<PyAny>> {
854    match expr {
855        AExpr::Element => Err(PyNotImplementedError::new_err("element")),
856        AExpr::Explode { expr, options } => Explode {
857            expr: expr.0,
858            options: (options.empty_as_null, options.keep_nulls),
859        }
860        .into_py_any(py),
861        AExpr::Column(name) => Column {
862            name: name.into_py_any(py)?,
863        }
864        .into_py_any(py),
865        AExpr::StructField(_) => Err(PyNotImplementedError::new_err("field")),
866        AExpr::Literal(lit) => {
867            use polars_core::prelude::AnyValue;
868            let dtype: Py<PyAny> = Wrap(lit.get_datatype()).into_py_any(py)?;
869            let py_value = match lit {
870                LiteralValue::Dyn(d) => match d {
871                    DynLiteralValue::Int(v) => v.into_py_any(py)?,
872                    DynLiteralValue::Float(v) => v.into_py_any(py)?,
873                    DynLiteralValue::Str(v) => v.into_py_any(py)?,
874                    DynLiteralValue::List(v) => dyn_list_literal_into_py(py, v)?,
875                },
876                LiteralValue::Scalar(sc) => {
877                    match sc.as_any_value() {
878                        // AnyValue conversion of duration to python's
879                        // datetime.timedelta drops nanoseconds because
880                        // there is no support for them. See
881                        // https://github.com/python/cpython/issues/59648
882                        AnyValue::Duration(delta, _) => delta.into_py_any(py)?,
883                        any => Wrap(any).into_py_any(py)?,
884                    }
885                },
886                LiteralValue::Range(range) => (range.low, range.high).into_py_any(py)?,
887                LiteralValue::Series(s) => PySeries::new((**s).clone()).into_py_any(py)?,
888            };
889
890            Literal {
891                value: py_value,
892                dtype,
893            }
894        }
895        .into_py_any(py),
896        AExpr::BinaryExpr { left, op, right } => BinaryExpr {
897            left: left.0,
898            op: Wrap(*op).into_py_any(py)?,
899            right: right.0,
900        }
901        .into_py_any(py),
902        AExpr::Cast {
903            expr,
904            dtype,
905            options,
906        } => Cast {
907            expr: expr.0,
908            dtype: Wrap(dtype.clone()).into_py_any(py)?,
909            options: *options as u8,
910        }
911        .into_py_any(py),
912        AExpr::Sort { expr, options } => Sort {
913            expr: expr.0,
914            options: (
915                options.maintain_order,
916                options.nulls_last,
917                options.descending,
918            ),
919        }
920        .into_py_any(py),
921        AExpr::Gather {
922            expr,
923            idx,
924            returns_scalar,
925            null_on_oob: _,
926        } => Gather {
927            expr: expr.0,
928            idx: idx.0,
929            scalar: *returns_scalar,
930        }
931        .into_py_any(py),
932        AExpr::Filter { input, by } => Filter {
933            input: input.0,
934            by: by.0,
935        }
936        .into_py_any(py),
937        AExpr::SortBy {
938            expr,
939            by,
940            sort_options,
941        } => SortBy {
942            expr: expr.0,
943            by: by.iter().map(|n| n.0).collect(),
944            sort_options: (
945                sort_options.maintain_order,
946                sort_options.nulls_last.clone(),
947                sort_options.descending.clone(),
948            ),
949        }
950        .into_py_any(py),
951        AExpr::Agg(aggexpr) => match aggexpr {
952            IRAggExpr::Min {
953                input,
954                propagate_nans,
955            } => Agg {
956                name: "min".into_py_any(py)?,
957                arguments: vec![input.0],
958                options: propagate_nans.into_py_any(py)?,
959            },
960            IRAggExpr::Max {
961                input,
962                propagate_nans,
963            } => Agg {
964                name: "max".into_py_any(py)?,
965                arguments: vec![input.0],
966                options: propagate_nans.into_py_any(py)?,
967            },
968            IRAggExpr::Median(n) => Agg {
969                name: "median".into_py_any(py)?,
970                arguments: vec![n.0],
971                options: py.None(),
972            },
973            IRAggExpr::NUnique(n) => Agg {
974                name: "n_unique".into_py_any(py)?,
975                arguments: vec![n.0],
976                options: py.None(),
977            },
978            IRAggExpr::First(n) => Agg {
979                name: "first".into_py_any(py)?,
980                arguments: vec![n.0],
981                options: py.None(),
982            },
983            IRAggExpr::FirstNonNull(n) => Agg {
984                name: "first_non_null".into_py_any(py)?,
985                arguments: vec![n.0],
986                options: py.None(),
987            },
988            IRAggExpr::Last(n) => Agg {
989                name: "last".into_py_any(py)?,
990                arguments: vec![n.0],
991                options: py.None(),
992            },
993            IRAggExpr::LastNonNull(n) => Agg {
994                name: "last_non_null".into_py_any(py)?,
995                arguments: vec![n.0],
996                options: py.None(),
997            },
998            IRAggExpr::Item {
999                input: n,
1000                allow_empty,
1001            } => Agg {
1002                name: "item".into_py_any(py)?,
1003                arguments: vec![n.0],
1004                options: allow_empty.into_py_any(py)?,
1005            },
1006            IRAggExpr::Mean(n) => Agg {
1007                name: "mean".into_py_any(py)?,
1008                arguments: vec![n.0],
1009                options: py.None(),
1010            },
1011            IRAggExpr::Implode {
1012                input: n,
1013                maintain_order,
1014            } => Agg {
1015                name: "implode".into_py_any(py)?,
1016                arguments: vec![n.0],
1017                options: maintain_order.into_py_any(py)?,
1018            },
1019            IRAggExpr::Sum(n) => Agg {
1020                name: "sum".into_py_any(py)?,
1021                arguments: vec![n.0],
1022                options: py.None(),
1023            },
1024            IRAggExpr::Count {
1025                input: n,
1026                include_nulls,
1027            } => Agg {
1028                name: "count".into_py_any(py)?,
1029                arguments: vec![n.0],
1030                options: include_nulls.into_py_any(py)?,
1031            },
1032            IRAggExpr::Std(n, ddof) => Agg {
1033                name: "std".into_py_any(py)?,
1034                arguments: vec![n.0],
1035                options: ddof.into_py_any(py)?,
1036            },
1037            IRAggExpr::Var(n, ddof) => Agg {
1038                name: "var".into_py_any(py)?,
1039                arguments: vec![n.0],
1040                options: ddof.into_py_any(py)?,
1041            },
1042            IRAggExpr::AggGroups(n) => Agg {
1043                name: "agg_groups".into_py_any(py)?,
1044                arguments: vec![n.0],
1045                options: py.None(),
1046            },
1047        }
1048        .into_py_any(py),
1049        AExpr::Ternary {
1050            predicate,
1051            truthy,
1052            falsy,
1053        } => Ternary {
1054            predicate: predicate.0,
1055            truthy: truthy.0,
1056            falsy: falsy.0,
1057        }
1058        .into_py_any(py),
1059        AExpr::AnonymousFunction { .. } => Err(PyNotImplementedError::new_err("anonymousfunction")),
1060        AExpr::AnonymousAgg { .. } => {
1061            Err(PyNotImplementedError::new_err("anonymous_streaming_agg"))
1062        },
1063        AExpr::Function {
1064            input,
1065            function,
1066            // TODO: expose options
1067            options: _,
1068        } => Function {
1069            input: input.iter().map(|n| n.node().0).collect(),
1070            function_data: match function {
1071                IRFunctionExpr::ArrayExpr(f) => match f {
1072                    IRArrayFunction::Length => (PyArrayFunction::Length,).into_py_any(py),
1073                    IRArrayFunction::Min => (PyArrayFunction::Min,).into_py_any(py),
1074                    IRArrayFunction::Max => (PyArrayFunction::Max,).into_py_any(py),
1075                    IRArrayFunction::Sum => (PyArrayFunction::Sum,).into_py_any(py),
1076                    IRArrayFunction::ToList => (PyArrayFunction::ToList,).into_py_any(py),
1077                    IRArrayFunction::Std(ddof) => (PyArrayFunction::Std, *ddof).into_py_any(py),
1078                    IRArrayFunction::Var(ddof) => (PyArrayFunction::Var, *ddof).into_py_any(py),
1079                    IRArrayFunction::Mean => (PyArrayFunction::Mean,).into_py_any(py),
1080                    IRArrayFunction::Median => (PyArrayFunction::Median,).into_py_any(py),
1081                    IRArrayFunction::Sort(options) => (
1082                        PyArrayFunction::Sort,
1083                        options.descending,
1084                        options.nulls_last,
1085                    )
1086                        .into_py_any(py),
1087                    IRArrayFunction::ArgMin => (PyArrayFunction::ArgMin,).into_py_any(py),
1088                    IRArrayFunction::ArgMax => (PyArrayFunction::ArgMax,).into_py_any(py),
1089                    IRArrayFunction::Get(null_on_oob) => {
1090                        (PyArrayFunction::Get, *null_on_oob).into_py_any(py)
1091                    },
1092                    IRArrayFunction::Join(ignore_nulls) => {
1093                        (PyArrayFunction::Join, *ignore_nulls).into_py_any(py)
1094                    },
1095                    #[cfg(feature = "is_in")]
1096                    IRArrayFunction::Contains { nulls_equal } => {
1097                        (PyArrayFunction::Contains, *nulls_equal).into_py_any(py)
1098                    },
1099                    #[cfg(feature = "array_count")]
1100                    IRArrayFunction::CountMatches => {
1101                        (PyArrayFunction::CountMatches,).into_py_any(py)
1102                    },
1103                    IRArrayFunction::Shift => (PyArrayFunction::Shift,).into_py_any(py),
1104                    IRArrayFunction::Explode(options) => (
1105                        PyArrayFunction::Explode,
1106                        options.empty_as_null,
1107                        options.keep_nulls,
1108                    )
1109                        .into_py_any(py),
1110                    IRArrayFunction::Concat => (PyArrayFunction::Concat,).into_py_any(py),
1111                    IRArrayFunction::Slice(offset, length) => {
1112                        (PyArrayFunction::Slice, *offset, *length).into_py_any(py)
1113                    },
1114                    // Callable name generators are deprecated in favor of explicit field names and
1115                    // cannot be represented in the typed view.
1116                    IRArrayFunction::ToStruct(name_generator) => match name_generator {
1117                        None => (PyArrayFunction::ToStruct, py.None()).into_py_any(py),
1118                        Some(_) => {
1119                            return Err(PyNotImplementedError::new_err(
1120                                "array to_struct with a name generator",
1121                            ));
1122                        },
1123                    },
1124                },
1125                IRFunctionExpr::BinaryExpr(_) => {
1126                    return Err(PyNotImplementedError::new_err("binary expr"));
1127                },
1128                IRFunctionExpr::Categorical(_) => {
1129                    return Err(PyNotImplementedError::new_err("categorical expr"));
1130                },
1131                IRFunctionExpr::Extension(_) => {
1132                    return Err(PyNotImplementedError::new_err("extension expr"));
1133                },
1134                IRFunctionExpr::ListExpr(listfun) => match listfun {
1135                    IRListFunction::Concat => (PyListFunction::Concat,).into_py_any(py),
1136                    #[cfg(feature = "is_in")]
1137                    IRListFunction::Contains { nulls_equal } => {
1138                        (PyListFunction::Contains, nulls_equal).into_py_any(py)
1139                    },
1140                    #[cfg(feature = "list_drop_nulls")]
1141                    IRListFunction::DropNulls => (PyListFunction::DropNulls,).into_py_any(py),
1142                    IRListFunction::Get(null_on_oob) => {
1143                        (PyListFunction::Get, null_on_oob).into_py_any(py)
1144                    },
1145                    IRListFunction::Length => (PyListFunction::Length,).into_py_any(py),
1146                    IRListFunction::Sort(options) => {
1147                        (PyListFunction::Sort, options.descending, options.nulls_last)
1148                            .into_py_any(py)
1149                    },
1150                    #[cfg(feature = "list_sets")]
1151                    IRListFunction::SetOperation(set_operation) => (
1152                        PyListFunction::SetOperation,
1153                        Into::<&str>::into(set_operation),
1154                    )
1155                        .into_py_any(py),
1156                    #[cfg(feature = "list_sample")]
1157                    IRListFunction::Sample {
1158                        is_fraction,
1159                        with_replacement,
1160                        shuffle,
1161                        seed,
1162                    } => (
1163                        PyListFunction::Sample,
1164                        is_fraction,
1165                        with_replacement,
1166                        shuffle,
1167                        seed,
1168                    )
1169                        .into_py_any(py),
1170                    IRListFunction::Slice => (PyListFunction::Slice,).into_py_any(py),
1171                    IRListFunction::Shift => (PyListFunction::Shift,).into_py_any(py),
1172                    #[cfg(feature = "list_gather")]
1173                    IRListFunction::Gather(null_on_oob) => {
1174                        (PyListFunction::Gather, null_on_oob).into_py_any(py)
1175                    },
1176                    #[cfg(feature = "list_gather")]
1177                    IRListFunction::GatherEvery => (PyListFunction::GatherEvery,).into_py_any(py),
1178                    #[cfg(feature = "list_count")]
1179                    IRListFunction::CountMatches => (PyListFunction::CountMatches,).into_py_any(py),
1180                    IRListFunction::Sum => (PyListFunction::Sum,).into_py_any(py),
1181                    IRListFunction::Max => (PyListFunction::Max,).into_py_any(py),
1182                    IRListFunction::Min => (PyListFunction::Min,).into_py_any(py),
1183                    IRListFunction::Mean => (PyListFunction::Mean,).into_py_any(py),
1184                    IRListFunction::Median => (PyListFunction::Median,).into_py_any(py),
1185                    IRListFunction::Std(ddof) => (PyListFunction::Std, ddof).into_py_any(py),
1186                    IRListFunction::Var(ddof) => (PyListFunction::Var, ddof).into_py_any(py),
1187                    IRListFunction::ArgMin => (PyListFunction::ArgMin,).into_py_any(py),
1188                    IRListFunction::ArgMax => (PyListFunction::ArgMax,).into_py_any(py),
1189                    IRListFunction::Diff { n, null_behavior } => (
1190                        PyListFunction::Diff,
1191                        n,
1192                        match null_behavior {
1193                            NullBehavior::Drop => "drop",
1194                            NullBehavior::Ignore => "ignore",
1195                        },
1196                    )
1197                        .into_py_any(py),
1198                    IRListFunction::Join(ignore_nulls) => {
1199                        (PyListFunction::Join, ignore_nulls).into_py_any(py)
1200                    },
1201                    #[cfg(feature = "dtype-array")]
1202                    IRListFunction::ToArray(width) => {
1203                        (PyListFunction::ToArray, width).into_py_any(py)
1204                    },
1205                    IRListFunction::ToStruct(names) => (
1206                        PyListFunction::ToStruct,
1207                        names.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
1208                    )
1209                        .into_py_any(py),
1210                },
1211                IRFunctionExpr::Bitwise(bitwisefun) => {
1212                    let py_function = match bitwisefun {
1213                        IRBitwiseFunction::CountOnes => PyBitwiseFunction::CountOnes,
1214                        IRBitwiseFunction::CountZeros => PyBitwiseFunction::CountZeros,
1215                        IRBitwiseFunction::LeadingOnes => PyBitwiseFunction::LeadingOnes,
1216                        IRBitwiseFunction::LeadingZeros => PyBitwiseFunction::LeadingZeros,
1217                        IRBitwiseFunction::TrailingOnes => PyBitwiseFunction::TrailingOnes,
1218                        IRBitwiseFunction::TrailingZeros => PyBitwiseFunction::TrailingZeros,
1219                        IRBitwiseFunction::And => PyBitwiseFunction::And,
1220                        IRBitwiseFunction::Or => PyBitwiseFunction::Or,
1221                        IRBitwiseFunction::Xor => PyBitwiseFunction::Xor,
1222                    };
1223                    (py_function,).into_py_any(py)
1224                },
1225                IRFunctionExpr::StringExpr(strfun) => match strfun {
1226                    IRStringFunction::Format { format, insertions } => (
1227                        PyStringFunction::Format,
1228                        format.as_str(),
1229                        insertions.to_vec(),
1230                    )
1231                        .into_py_any(py),
1232                    IRStringFunction::ConcatHorizontal {
1233                        delimiter,
1234                        ignore_nulls,
1235                    } => (
1236                        PyStringFunction::ConcatHorizontal,
1237                        delimiter.as_str(),
1238                        ignore_nulls,
1239                    )
1240                        .into_py_any(py),
1241                    IRStringFunction::ConcatVertical {
1242                        delimiter,
1243                        ignore_nulls,
1244                    } => (
1245                        PyStringFunction::ConcatVertical,
1246                        delimiter.as_str(),
1247                        ignore_nulls,
1248                    )
1249                        .into_py_any(py),
1250                    #[cfg(feature = "regex")]
1251                    IRStringFunction::Contains { literal, strict } => {
1252                        (PyStringFunction::Contains, literal, strict).into_py_any(py)
1253                    },
1254                    IRStringFunction::CountMatches(literal) => {
1255                        (PyStringFunction::CountMatches, literal).into_py_any(py)
1256                    },
1257                    IRStringFunction::EndsWith => (PyStringFunction::EndsWith,).into_py_any(py),
1258                    IRStringFunction::Extract(group_index) => {
1259                        (PyStringFunction::Extract, group_index).into_py_any(py)
1260                    },
1261                    IRStringFunction::ExtractAll => (PyStringFunction::ExtractAll,).into_py_any(py),
1262                    #[cfg(feature = "extract_groups")]
1263                    IRStringFunction::ExtractGroups { dtype, pat } => (
1264                        PyStringFunction::ExtractGroups,
1265                        &Wrap(dtype.clone()),
1266                        pat.as_str(),
1267                    )
1268                        .into_py_any(py),
1269                    #[cfg(feature = "regex")]
1270                    IRStringFunction::Find { literal, strict } => {
1271                        (PyStringFunction::Find, literal, strict).into_py_any(py)
1272                    },
1273                    IRStringFunction::ToInteger { dtype: _, strict } => {
1274                        (PyStringFunction::ToInteger, strict).into_py_any(py)
1275                    },
1276                    IRStringFunction::LenBytes => (PyStringFunction::LenBytes,).into_py_any(py),
1277                    IRStringFunction::LenChars => (PyStringFunction::LenChars,).into_py_any(py),
1278                    IRStringFunction::Lowercase => (PyStringFunction::Lowercase,).into_py_any(py),
1279                    #[cfg(feature = "extract_jsonpath")]
1280                    IRStringFunction::JsonDecode(_) => {
1281                        (PyStringFunction::JsonDecode, <Option<usize>>::None).into_py_any(py)
1282                    },
1283                    #[cfg(feature = "extract_jsonpath")]
1284                    IRStringFunction::JsonPathMatch => {
1285                        (PyStringFunction::JsonPathMatch,).into_py_any(py)
1286                    },
1287                    #[cfg(feature = "regex")]
1288                    IRStringFunction::Replace { n, literal } => {
1289                        (PyStringFunction::Replace, n, literal).into_py_any(py)
1290                    },
1291                    #[cfg(feature = "string_normalize")]
1292                    IRStringFunction::Normalize { form } => (
1293                        PyStringFunction::Normalize,
1294                        match form {
1295                            UnicodeForm::NFC => "nfc",
1296                            UnicodeForm::NFKC => "nfkc",
1297                            UnicodeForm::NFD => "nfd",
1298                            UnicodeForm::NFKD => "nfkd",
1299                        },
1300                    )
1301                        .into_py_any(py),
1302                    IRStringFunction::Reverse => (PyStringFunction::Reverse,).into_py_any(py),
1303                    IRStringFunction::PadStart { fill_char } => {
1304                        (PyStringFunction::PadStart, fill_char).into_py_any(py)
1305                    },
1306                    IRStringFunction::PadEnd { fill_char } => {
1307                        (PyStringFunction::PadEnd, fill_char).into_py_any(py)
1308                    },
1309                    IRStringFunction::Slice => (PyStringFunction::Slice,).into_py_any(py),
1310                    IRStringFunction::Head => (PyStringFunction::Head,).into_py_any(py),
1311                    IRStringFunction::Tail => (PyStringFunction::Tail,).into_py_any(py),
1312                    IRStringFunction::HexEncode => (PyStringFunction::HexEncode,).into_py_any(py),
1313                    #[cfg(feature = "binary_encoding")]
1314                    IRStringFunction::HexDecode(strict) => {
1315                        (PyStringFunction::HexDecode, strict).into_py_any(py)
1316                    },
1317                    IRStringFunction::Base64Encode => {
1318                        (PyStringFunction::Base64Encode,).into_py_any(py)
1319                    },
1320                    #[cfg(feature = "binary_encoding")]
1321                    IRStringFunction::Base64Decode(strict) => {
1322                        (PyStringFunction::Base64Decode, strict).into_py_any(py)
1323                    },
1324                    IRStringFunction::StartsWith => (PyStringFunction::StartsWith,).into_py_any(py),
1325                    IRStringFunction::StripChars => (PyStringFunction::StripChars,).into_py_any(py),
1326                    IRStringFunction::StripCharsStart => {
1327                        (PyStringFunction::StripCharsStart,).into_py_any(py)
1328                    },
1329                    IRStringFunction::StripCharsEnd => {
1330                        (PyStringFunction::StripCharsEnd,).into_py_any(py)
1331                    },
1332                    IRStringFunction::StripPrefix => {
1333                        (PyStringFunction::StripPrefix,).into_py_any(py)
1334                    },
1335                    IRStringFunction::StripSuffix => {
1336                        (PyStringFunction::StripSuffix,).into_py_any(py)
1337                    },
1338                    IRStringFunction::SplitExact { n, inclusive } => {
1339                        (PyStringFunction::SplitExact, n, inclusive).into_py_any(py)
1340                    },
1341                    IRStringFunction::SplitN(n) => (PyStringFunction::SplitN, n).into_py_any(py),
1342                    IRStringFunction::Strptime(_, options) => (
1343                        PyStringFunction::Strptime,
1344                        options.format.as_ref().map(|s| s.as_str()),
1345                        options.strict,
1346                        options.exact,
1347                        options.cache,
1348                    )
1349                        .into_py_any(py),
1350                    IRStringFunction::Split(inclusive) => {
1351                        (PyStringFunction::Split, inclusive).into_py_any(py)
1352                    },
1353                    IRStringFunction::SplitRegex { inclusive, strict } => {
1354                        (PyStringFunction::SplitRegex, inclusive, strict).into_py_any(py)
1355                    },
1356                    IRStringFunction::ToDecimal { scale } => {
1357                        (PyStringFunction::ToDecimal, scale).into_py_any(py)
1358                    },
1359                    #[cfg(feature = "nightly")]
1360                    IRStringFunction::Titlecase => (PyStringFunction::Titlecase,).into_py_any(py),
1361                    IRStringFunction::Uppercase => (PyStringFunction::Uppercase,).into_py_any(py),
1362                    IRStringFunction::ZFill => (PyStringFunction::ZFill,).into_py_any(py),
1363                    #[cfg(feature = "find_many")]
1364                    IRStringFunction::ContainsAny {
1365                        ascii_case_insensitive,
1366                    } => (PyStringFunction::ContainsAny, ascii_case_insensitive).into_py_any(py),
1367                    #[cfg(feature = "find_many")]
1368                    IRStringFunction::ReplaceMany {
1369                        ascii_case_insensitive,
1370                        leftmost,
1371                    } => (
1372                        PyStringFunction::ReplaceMany,
1373                        ascii_case_insensitive,
1374                        leftmost,
1375                    )
1376                        .into_py_any(py),
1377                    #[cfg(feature = "find_many")]
1378                    IRStringFunction::ExtractMany {
1379                        ascii_case_insensitive,
1380                        overlapping,
1381                        leftmost,
1382                    } => (
1383                        PyStringFunction::ExtractMany,
1384                        ascii_case_insensitive,
1385                        overlapping,
1386                        leftmost,
1387                    )
1388                        .into_py_any(py),
1389                    #[cfg(feature = "find_many")]
1390                    IRStringFunction::FindMany {
1391                        ascii_case_insensitive,
1392                        overlapping,
1393                        leftmost,
1394                    } => (
1395                        PyStringFunction::FindMany,
1396                        ascii_case_insensitive,
1397                        overlapping,
1398                        leftmost,
1399                    )
1400                        .into_py_any(py),
1401                    #[cfg(feature = "regex")]
1402                    IRStringFunction::EscapeRegex => {
1403                        (PyStringFunction::EscapeRegex,).into_py_any(py)
1404                    },
1405                },
1406                IRFunctionExpr::StructExpr(fun) => match fun {
1407                    IRStructFunction::FieldByName(name) => {
1408                        (PyStructFunction::FieldByName, name.as_str()).into_py_any(py)
1409                    },
1410                    IRStructFunction::RenameFields(names) => (
1411                        PyStructFunction::RenameFields,
1412                        names.iter().map(|s| s.as_str()).collect_vec(),
1413                    )
1414                        .into_py_any(py),
1415                    IRStructFunction::DropFields(names, strict) => (
1416                        PyStructFunction::DropFields,
1417                        names.iter().map(|s| s.as_str()).collect_vec(),
1418                        strict,
1419                    )
1420                        .into_py_any(py),
1421                    IRStructFunction::PrefixFields(prefix) => {
1422                        (PyStructFunction::PrefixFields, prefix.as_str()).into_py_any(py)
1423                    },
1424                    IRStructFunction::SuffixFields(prefix) => {
1425                        (PyStructFunction::SuffixFields, prefix.as_str()).into_py_any(py)
1426                    },
1427                    #[cfg(feature = "json")]
1428                    IRStructFunction::JsonEncode => (PyStructFunction::JsonEncode,).into_py_any(py),
1429                    IRStructFunction::MapFieldNames(function) => match function {
1430                        PlanCallback::Python(lambda) => {
1431                            (PyStructFunction::MapFieldNames, lambda.0.clone_ref(py))
1432                                .into_py_any(py)
1433                        },
1434                        PlanCallback::Rust(_) => {
1435                            return Err(PyNotImplementedError::new_err(
1436                                "map_field_names with rust callback",
1437                            ));
1438                        },
1439                    },
1440                },
1441                IRFunctionExpr::TemporalExpr(fun) => match fun {
1442                    IRTemporalFunction::Millennium => {
1443                        (PyTemporalFunction::Millennium,).into_py_any(py)
1444                    },
1445                    IRTemporalFunction::Century => (PyTemporalFunction::Century,).into_py_any(py),
1446                    IRTemporalFunction::Year => (PyTemporalFunction::Year,).into_py_any(py),
1447                    IRTemporalFunction::IsLeapYear => {
1448                        (PyTemporalFunction::IsLeapYear,).into_py_any(py)
1449                    },
1450                    IRTemporalFunction::IsoYear => (PyTemporalFunction::IsoYear,).into_py_any(py),
1451                    IRTemporalFunction::Quarter => (PyTemporalFunction::Quarter,).into_py_any(py),
1452                    IRTemporalFunction::Month => (PyTemporalFunction::Month,).into_py_any(py),
1453                    IRTemporalFunction::Week => (PyTemporalFunction::Week,).into_py_any(py),
1454                    IRTemporalFunction::WeekDay => (PyTemporalFunction::WeekDay,).into_py_any(py),
1455                    IRTemporalFunction::Day => (PyTemporalFunction::Day,).into_py_any(py),
1456                    IRTemporalFunction::OrdinalDay => {
1457                        (PyTemporalFunction::OrdinalDay,).into_py_any(py)
1458                    },
1459                    IRTemporalFunction::Time => (PyTemporalFunction::Time,).into_py_any(py),
1460                    IRTemporalFunction::Date => (PyTemporalFunction::Date,).into_py_any(py),
1461                    IRTemporalFunction::Datetime => (PyTemporalFunction::Datetime,).into_py_any(py),
1462                    IRTemporalFunction::Duration(time_unit) => {
1463                        (PyTemporalFunction::Duration, Wrap(*time_unit)).into_py_any(py)
1464                    },
1465                    IRTemporalFunction::Hour => (PyTemporalFunction::Hour,).into_py_any(py),
1466                    IRTemporalFunction::Minute => (PyTemporalFunction::Minute,).into_py_any(py),
1467                    IRTemporalFunction::Second => (PyTemporalFunction::Second,).into_py_any(py),
1468                    IRTemporalFunction::Millisecond => {
1469                        (PyTemporalFunction::Millisecond,).into_py_any(py)
1470                    },
1471                    IRTemporalFunction::Microsecond => {
1472                        (PyTemporalFunction::Microsecond,).into_py_any(py)
1473                    },
1474                    IRTemporalFunction::Nanosecond => {
1475                        (PyTemporalFunction::Nanosecond,).into_py_any(py)
1476                    },
1477                    IRTemporalFunction::DaysInMonth => {
1478                        (PyTemporalFunction::DaysInMonth,).into_py_any(py)
1479                    },
1480                    IRTemporalFunction::TotalDays { fractional } => {
1481                        (PyTemporalFunction::TotalDays, fractional).into_py_any(py)
1482                    },
1483                    IRTemporalFunction::TotalHours { fractional } => {
1484                        (PyTemporalFunction::TotalHours, fractional).into_py_any(py)
1485                    },
1486                    IRTemporalFunction::TotalMinutes { fractional } => {
1487                        (PyTemporalFunction::TotalMinutes, fractional).into_py_any(py)
1488                    },
1489                    IRTemporalFunction::TotalSeconds { fractional } => {
1490                        (PyTemporalFunction::TotalSeconds, fractional).into_py_any(py)
1491                    },
1492                    IRTemporalFunction::TotalMilliseconds { fractional } => {
1493                        (PyTemporalFunction::TotalMilliseconds, fractional).into_py_any(py)
1494                    },
1495                    IRTemporalFunction::TotalMicroseconds { fractional } => {
1496                        (PyTemporalFunction::TotalMicroseconds, fractional).into_py_any(py)
1497                    },
1498                    IRTemporalFunction::TotalNanoseconds { fractional } => {
1499                        (PyTemporalFunction::TotalNanoseconds, fractional).into_py_any(py)
1500                    },
1501                    IRTemporalFunction::ToString(format) => {
1502                        (PyTemporalFunction::ToString, format).into_py_any(py)
1503                    },
1504                    IRTemporalFunction::CastTimeUnit(time_unit) => {
1505                        (PyTemporalFunction::CastTimeUnit, Wrap(*time_unit)).into_py_any(py)
1506                    },
1507                    IRTemporalFunction::WithTimeUnit(time_unit) => {
1508                        (PyTemporalFunction::WithTimeUnit, Wrap(*time_unit)).into_py_any(py)
1509                    },
1510                    #[cfg(feature = "timezones")]
1511                    IRTemporalFunction::ConvertTimeZone(time_zone) => {
1512                        (PyTemporalFunction::ConvertTimeZone, time_zone.as_str()).into_py_any(py)
1513                    },
1514                    IRTemporalFunction::TimeStamp(time_unit) => {
1515                        (PyTemporalFunction::TimeStamp, Wrap(*time_unit)).into_py_any(py)
1516                    },
1517                    IRTemporalFunction::Truncate => (PyTemporalFunction::Truncate,).into_py_any(py),
1518                    IRTemporalFunction::OffsetBy => (PyTemporalFunction::OffsetBy,).into_py_any(py),
1519                    IRTemporalFunction::MonthStart => {
1520                        (PyTemporalFunction::MonthStart,).into_py_any(py)
1521                    },
1522                    IRTemporalFunction::MonthEnd => (PyTemporalFunction::MonthEnd,).into_py_any(py),
1523                    #[cfg(feature = "timezones")]
1524                    IRTemporalFunction::BaseUtcOffset => {
1525                        (PyTemporalFunction::BaseUtcOffset,).into_py_any(py)
1526                    },
1527                    #[cfg(feature = "timezones")]
1528                    IRTemporalFunction::DSTOffset => {
1529                        (PyTemporalFunction::DSTOffset,).into_py_any(py)
1530                    },
1531                    IRTemporalFunction::Round => (PyTemporalFunction::Round,).into_py_any(py),
1532                    IRTemporalFunction::Replace => (PyTemporalFunction::Replace).into_py_any(py),
1533                    #[cfg(feature = "timezones")]
1534                    IRTemporalFunction::ReplaceTimeZone(time_zone, non_existent) => (
1535                        PyTemporalFunction::ReplaceTimeZone,
1536                        time_zone.as_ref().map(|s| s.as_str()),
1537                        Into::<&str>::into(non_existent),
1538                    )
1539                        .into_py_any(py),
1540                    IRTemporalFunction::Combine(time_unit) => {
1541                        (PyTemporalFunction::Combine, Wrap(*time_unit)).into_py_any(py)
1542                    },
1543                    IRTemporalFunction::DatetimeFunction {
1544                        time_unit,
1545                        time_zone,
1546                    } => (
1547                        PyTemporalFunction::DatetimeFunction,
1548                        Wrap(*time_unit),
1549                        time_zone.as_ref().map(|s| s.as_str()),
1550                    )
1551                        .into_py_any(py),
1552                },
1553                IRFunctionExpr::Boolean(boolfun) => match boolfun {
1554                    IRBooleanFunction::Any { ignore_nulls } => {
1555                        (PyBooleanFunction::Any, *ignore_nulls).into_py_any(py)
1556                    },
1557                    IRBooleanFunction::All { ignore_nulls } => {
1558                        (PyBooleanFunction::All, *ignore_nulls).into_py_any(py)
1559                    },
1560                    IRBooleanFunction::IsEmpty { ignore_nulls } => {
1561                        (PyBooleanFunction::IsEmpty, *ignore_nulls).into_py_any(py)
1562                    },
1563                    IRBooleanFunction::HasNulls => (PyBooleanFunction::HasNulls,).into_py_any(py),
1564                    IRBooleanFunction::IsNull => (PyBooleanFunction::IsNull,).into_py_any(py),
1565                    IRBooleanFunction::IsNotNull => (PyBooleanFunction::IsNotNull,).into_py_any(py),
1566                    IRBooleanFunction::IsFinite => (PyBooleanFunction::IsFinite,).into_py_any(py),
1567                    IRBooleanFunction::IsInfinite => {
1568                        (PyBooleanFunction::IsInfinite,).into_py_any(py)
1569                    },
1570                    IRBooleanFunction::IsNan => (PyBooleanFunction::IsNan,).into_py_any(py),
1571                    IRBooleanFunction::IsNotNan => (PyBooleanFunction::IsNotNan,).into_py_any(py),
1572                    IRBooleanFunction::IsFirstDistinct => {
1573                        (PyBooleanFunction::IsFirstDistinct,).into_py_any(py)
1574                    },
1575                    IRBooleanFunction::IsLastDistinct => {
1576                        (PyBooleanFunction::IsLastDistinct,).into_py_any(py)
1577                    },
1578                    IRBooleanFunction::IsUnique => (PyBooleanFunction::IsUnique,).into_py_any(py),
1579                    IRBooleanFunction::IsDuplicated => {
1580                        (PyBooleanFunction::IsDuplicated,).into_py_any(py)
1581                    },
1582                    IRBooleanFunction::IsBetween { closed } => {
1583                        (PyBooleanFunction::IsBetween, Into::<&str>::into(closed)).into_py_any(py)
1584                    },
1585                    #[cfg(feature = "is_in")]
1586                    IRBooleanFunction::IsIn { nulls_equal } => {
1587                        (PyBooleanFunction::IsIn, nulls_equal).into_py_any(py)
1588                    },
1589                    IRBooleanFunction::IsClose {
1590                        abs_tol,
1591                        rel_tol,
1592                        nans_equal,
1593                    } => (PyBooleanFunction::IsClose, abs_tol.0, rel_tol.0, nans_equal)
1594                        .into_py_any(py),
1595                    IRBooleanFunction::IsSorted {
1596                        descending,
1597                        nulls_last,
1598                    } => (PyBooleanFunction::IsSorted, *descending, *nulls_last).into_py_any(py),
1599                    IRBooleanFunction::AllHorizontal => {
1600                        (PyBooleanFunction::AllHorizontal,).into_py_any(py)
1601                    },
1602                    IRBooleanFunction::AnyHorizontal => {
1603                        (PyBooleanFunction::AnyHorizontal,).into_py_any(py)
1604                    },
1605                    IRBooleanFunction::Not => (PyBooleanFunction::Not,).into_py_any(py),
1606                },
1607                IRFunctionExpr::Abs => ("abs",).into_py_any(py),
1608                #[cfg(feature = "hist")]
1609                IRFunctionExpr::Hist {
1610                    bin_count,
1611                    include_category,
1612                    include_breakpoint,
1613                } => ("hist", bin_count, include_category, include_breakpoint).into_py_any(py),
1614                IRFunctionExpr::NullCount => ("null_count",).into_py_any(py),
1615                IRFunctionExpr::Pow(f) => match f {
1616                    IRPowFunction::Generic => ("pow",).into_py_any(py),
1617                    IRPowFunction::Sqrt => ("sqrt",).into_py_any(py),
1618                    IRPowFunction::Cbrt => ("cbrt",).into_py_any(py),
1619                },
1620                IRFunctionExpr::Hash(seed, seed_1, seed_2, seed_3) => {
1621                    ("hash", seed, seed_1, seed_2, seed_3).into_py_any(py)
1622                },
1623                IRFunctionExpr::ArgWhere => ("argwhere",).into_py_any(py),
1624                #[cfg(feature = "index_of")]
1625                IRFunctionExpr::IndexOf => ("index_of",).into_py_any(py),
1626                #[cfg(feature = "search_sorted")]
1627                IRFunctionExpr::SearchSorted { side, descending } => (
1628                    "search_sorted",
1629                    match side {
1630                        SearchSortedSide::Any => "any",
1631                        SearchSortedSide::Left => "left",
1632                        SearchSortedSide::Right => "right",
1633                    },
1634                    descending,
1635                )
1636                    .into_py_any(py),
1637                IRFunctionExpr::Range(rangefun) => match rangefun {
1638                    IRRangeFunction::IntRange { step, dtype } => {
1639                        (PyRangeFunction::IntRange, step, &Wrap(dtype.clone())).into_py_any(py)
1640                    },
1641                    IRRangeFunction::IntRanges { dtype } => {
1642                        (PyRangeFunction::IntRanges, &Wrap(dtype.clone())).into_py_any(py)
1643                    },
1644                    IRRangeFunction::LinearSpace { closed } => (
1645                        PyRangeFunction::LinearSpace,
1646                        closed_interval_into_py(closed),
1647                    )
1648                        .into_py_any(py),
1649                    IRRangeFunction::LinearSpaces {
1650                        closed,
1651                        array_width,
1652                    } => (
1653                        PyRangeFunction::LinearSpaces,
1654                        closed_interval_into_py(closed),
1655                        array_width,
1656                    )
1657                        .into_py_any(py),
1658                    IRRangeFunction::DateRange {
1659                        interval,
1660                        closed,
1661                        arg_type,
1662                    } => (
1663                        PyRangeFunction::DateRange,
1664                        interval
1665                            .as_ref()
1666                            .map_or_else(|| Ok(py.None()), |d| Wrap(*d).into_py_any(py))?,
1667                        Wrap(*closed).into_py_any(py)?,
1668                        date_range_args_into_py(arg_type),
1669                    )
1670                        .into_py_any(py),
1671                    IRRangeFunction::DateRanges {
1672                        interval,
1673                        closed,
1674                        arg_type,
1675                    } => (
1676                        PyRangeFunction::DateRanges,
1677                        interval
1678                            .as_ref()
1679                            .map_or_else(|| Ok(py.None()), |d| Wrap(*d).into_py_any(py))?,
1680                        Wrap(*closed).into_py_any(py)?,
1681                        date_range_args_into_py(arg_type),
1682                    )
1683                        .into_py_any(py),
1684                    IRRangeFunction::DatetimeRange {
1685                        interval,
1686                        closed,
1687                        time_unit,
1688                        time_zone,
1689                        arg_type,
1690                    } => (
1691                        PyRangeFunction::DatetimeRange,
1692                        interval
1693                            .as_ref()
1694                            .map_or_else(|| Ok(py.None()), |d| Wrap(*d).into_py_any(py))?,
1695                        Wrap(*closed).into_py_any(py)?,
1696                        time_unit.map_or_else(|| Ok(py.None()), |tu| Wrap(tu).into_py_any(py))?,
1697                        time_zone.as_ref().map(|s| s.as_str()),
1698                        date_range_args_into_py(arg_type),
1699                    )
1700                        .into_py_any(py),
1701                    IRRangeFunction::DatetimeRanges {
1702                        interval,
1703                        closed,
1704                        time_unit,
1705                        time_zone,
1706                        arg_type,
1707                    } => (
1708                        PyRangeFunction::DatetimeRanges,
1709                        interval
1710                            .as_ref()
1711                            .map_or_else(|| Ok(py.None()), |d| Wrap(*d).into_py_any(py))?,
1712                        Wrap(*closed).into_py_any(py)?,
1713                        time_unit.map_or_else(|| Ok(py.None()), |tu| Wrap(tu).into_py_any(py))?,
1714                        time_zone.as_ref().map(|s| s.as_str()),
1715                        date_range_args_into_py(arg_type),
1716                    )
1717                        .into_py_any(py),
1718                    IRRangeFunction::TimeRange { interval, closed } => (
1719                        PyRangeFunction::TimeRange,
1720                        Wrap(*interval).into_py_any(py)?,
1721                        Wrap(*closed).into_py_any(py)?,
1722                    )
1723                        .into_py_any(py),
1724                    IRRangeFunction::TimeRanges { interval, closed } => (
1725                        PyRangeFunction::TimeRanges,
1726                        Wrap(*interval).into_py_any(py)?,
1727                        Wrap(*closed).into_py_any(py)?,
1728                    )
1729                        .into_py_any(py),
1730                },
1731                #[cfg(feature = "trigonometry")]
1732                IRFunctionExpr::Trigonometry(trigfun) => {
1733                    use polars_plan::plans::IRTrigonometricFunction;
1734
1735                    match trigfun {
1736                        IRTrigonometricFunction::Cos => ("cos",),
1737                        IRTrigonometricFunction::Cot => ("cot",),
1738                        IRTrigonometricFunction::Sin => ("sin",),
1739                        IRTrigonometricFunction::Tan => ("tan",),
1740                        IRTrigonometricFunction::ArcCos => ("arccos",),
1741                        IRTrigonometricFunction::ArcSin => ("arcsin",),
1742                        IRTrigonometricFunction::ArcTan => ("arctan",),
1743                        IRTrigonometricFunction::Cosh => ("cosh",),
1744                        IRTrigonometricFunction::Sinh => ("sinh",),
1745                        IRTrigonometricFunction::Tanh => ("tanh",),
1746                        IRTrigonometricFunction::ArcCosh => ("arccosh",),
1747                        IRTrigonometricFunction::ArcSinh => ("arcsinh",),
1748                        IRTrigonometricFunction::ArcTanh => ("arctanh",),
1749                        IRTrigonometricFunction::Degrees => ("degrees",),
1750                        IRTrigonometricFunction::Radians => ("radians",),
1751                    }
1752                    .into_py_any(py)
1753                },
1754                #[cfg(feature = "trigonometry")]
1755                IRFunctionExpr::Atan2 => ("atan2",).into_py_any(py),
1756                #[cfg(feature = "sign")]
1757                IRFunctionExpr::Sign => ("sign",).into_py_any(py),
1758                IRFunctionExpr::FillNull => ("fill_null",).into_py_any(py),
1759                IRFunctionExpr::RollingExpr { function, options } => match function {
1760                    IRRollingFunction::CorrCov {
1761                        corr_cov_options,
1762                        is_corr,
1763                    } => {
1764                        // Tuple consumed by external engines (e.g., cudf-polars):
1765                        // (RollingFunction, window_size, min_periods, ddof, is_corr)
1766                        (
1767                            PyRollingFunction::CorrCov,
1768                            corr_cov_options.window_size,
1769                            corr_cov_options.min_periods,
1770                            corr_cov_options.ddof,
1771                            is_corr,
1772                        )
1773                            .into_py_any(py)
1774                    },
1775                    IRRollingFunction::Map(_) => {
1776                        return Err(PyNotImplementedError::new_err("rolling map"));
1777                    },
1778                    _ => {
1779                        let py_function = match function {
1780                            IRRollingFunction::Min => PyRollingFunction::Min,
1781                            IRRollingFunction::Max => PyRollingFunction::Max,
1782                            IRRollingFunction::Mean => PyRollingFunction::Mean,
1783                            IRRollingFunction::Sum => PyRollingFunction::Sum,
1784                            IRRollingFunction::Quantile => PyRollingFunction::Quantile,
1785                            IRRollingFunction::Var => PyRollingFunction::Var,
1786                            IRRollingFunction::Std => PyRollingFunction::Std,
1787                            IRRollingFunction::Rank => PyRollingFunction::Rank,
1788                            IRRollingFunction::Skew => PyRollingFunction::Skew,
1789                            IRRollingFunction::Kurtosis => PyRollingFunction::Kurtosis,
1790                            IRRollingFunction::CorrCov { .. } | IRRollingFunction::Map(_) => {
1791                                unreachable!()
1792                            },
1793                        };
1794                        let fn_params = rolling_fn_params_into_py(py, &options.fn_params)?;
1795                        // Tuple consumed by external engines (e.g., cudf-polars):
1796                        // (RollingFunction, window_size, min_periods, weights, center, fn_params)
1797                        (
1798                            py_function,
1799                            options.window_size,
1800                            options.min_periods,
1801                            &options.weights,
1802                            options.center,
1803                            fn_params,
1804                        )
1805                            .into_py_any(py)
1806                    },
1807                },
1808                IRFunctionExpr::RollingExprBy {
1809                    function_by,
1810                    options,
1811                } => {
1812                    let py_function = match function_by {
1813                        IRRollingFunctionBy::MinBy => PyRollingFunctionBy::MinBy,
1814                        IRRollingFunctionBy::MaxBy => PyRollingFunctionBy::MaxBy,
1815                        IRRollingFunctionBy::MeanBy => PyRollingFunctionBy::MeanBy,
1816                        IRRollingFunctionBy::SumBy => PyRollingFunctionBy::SumBy,
1817                        IRRollingFunctionBy::QuantileBy => PyRollingFunctionBy::QuantileBy,
1818                        IRRollingFunctionBy::VarBy => PyRollingFunctionBy::VarBy,
1819                        IRRollingFunctionBy::StdBy => PyRollingFunctionBy::StdBy,
1820                        IRRollingFunctionBy::RankBy => PyRollingFunctionBy::RankBy,
1821                    };
1822                    let fn_params = rolling_fn_params_into_py(py, &options.fn_params)?;
1823                    // Tuple consumed by external engines (e.g., cudf-polars):
1824                    // (RollingFunctionBy, window_size, min_periods, closed_window, fn_params)
1825                    (
1826                        py_function,
1827                        Wrap(options.window_size),
1828                        options.min_periods,
1829                        Wrap(options.closed_window),
1830                        fn_params,
1831                    )
1832                        .into_py_any(py)
1833                },
1834                IRFunctionExpr::Rechunk => ("rechunk",).into_py_any(py),
1835                IRFunctionExpr::ShiftAndFill => ("shift_and_fill",).into_py_any(py),
1836                IRFunctionExpr::Shift => ("shift",).into_py_any(py),
1837                IRFunctionExpr::DropNans => ("drop_nans",).into_py_any(py),
1838                IRFunctionExpr::DropNulls => ("drop_nulls",).into_py_any(py),
1839                IRFunctionExpr::Quantile { method } => {
1840                    let method = match method {
1841                        QuantileMethod::Nearest => "nearest",
1842                        QuantileMethod::Lower => "lower",
1843                        QuantileMethod::Higher => "higher",
1844                        QuantileMethod::Midpoint => "midpoint",
1845                        QuantileMethod::Linear => "linear",
1846                        QuantileMethod::Equiprobable => "equiprobable",
1847                    };
1848                    ("quantile", method).into_py_any(py)
1849                },
1850                IRFunctionExpr::Mode { maintain_order } => {
1851                    ("mode", *maintain_order).into_py_any(py)
1852                },
1853                IRFunctionExpr::Skew(bias) => ("skew", bias).into_py_any(py),
1854                IRFunctionExpr::Kurtosis(fisher, bias) => {
1855                    ("kurtosis", fisher, bias).into_py_any(py)
1856                },
1857                IRFunctionExpr::Reshape(_) => {
1858                    return Err(PyNotImplementedError::new_err("reshape"));
1859                },
1860                #[cfg(feature = "repeat_by")]
1861                IRFunctionExpr::RepeatBy => ("repeat_by",).into_py_any(py),
1862                IRFunctionExpr::ArgUnique => ("arg_unique",).into_py_any(py),
1863                IRFunctionExpr::ArgMin => ("arg_min",).into_py_any(py),
1864                IRFunctionExpr::ArgMax => ("arg_max",).into_py_any(py),
1865                IRFunctionExpr::MinBy => ("min_by",).into_py_any(py),
1866                IRFunctionExpr::MaxBy => ("max_by",).into_py_any(py),
1867                IRFunctionExpr::ArgSort {
1868                    descending,
1869                    nulls_last,
1870                } => ("arg_max", descending, nulls_last).into_py_any(py),
1871                IRFunctionExpr::Product => ("product",).into_py_any(py),
1872                IRFunctionExpr::Repeat => ("repeat",).into_py_any(py),
1873                IRFunctionExpr::Rank { options, seed } => {
1874                    let method = match options.method {
1875                        RankMethod::Average => "average",
1876                        RankMethod::Min => "min",
1877                        RankMethod::Max => "max",
1878                        RankMethod::Dense => "dense",
1879                        RankMethod::Ordinal => "ordinal",
1880                        RankMethod::Random => "random",
1881                    };
1882                    ("rank", method, options.descending, seed.map(|s| s as i64)).into_py_any(py)
1883                },
1884                IRFunctionExpr::Clip { has_min, has_max } => {
1885                    ("clip", has_min, has_max).into_py_any(py)
1886                },
1887                IRFunctionExpr::AsList => ("as_list",).into_py_any(py),
1888                IRFunctionExpr::AsStruct => ("as_struct",).into_py_any(py),
1889                #[cfg(feature = "top_k")]
1890                IRFunctionExpr::TopK { descending } => ("top_k", descending).into_py_any(py),
1891                IRFunctionExpr::CumCount { reverse } => ("cum_count", reverse).into_py_any(py),
1892                IRFunctionExpr::CumSum { reverse } => ("cum_sum", reverse).into_py_any(py),
1893                IRFunctionExpr::CumProd { reverse } => ("cum_prod", reverse).into_py_any(py),
1894                IRFunctionExpr::CumMin { reverse } => ("cum_min", reverse).into_py_any(py),
1895                IRFunctionExpr::CumMax { reverse } => ("cum_max", reverse).into_py_any(py),
1896                IRFunctionExpr::Reverse => ("reverse",).into_py_any(py),
1897                IRFunctionExpr::ValueCounts {
1898                    sort,
1899                    parallel,
1900                    name,
1901                    normalize,
1902                } => ("value_counts", sort, parallel, name.as_str(), normalize).into_py_any(py),
1903                IRFunctionExpr::UniqueCounts => ("unique_counts",).into_py_any(py),
1904                IRFunctionExpr::ApproxNUnique => ("approx_n_unique",).into_py_any(py),
1905                IRFunctionExpr::Coalesce => ("coalesce",).into_py_any(py),
1906                IRFunctionExpr::Diff(null_behaviour) => (
1907                    "diff",
1908                    match null_behaviour {
1909                        NullBehavior::Drop => "drop",
1910                        NullBehavior::Ignore => "ignore",
1911                    },
1912                )
1913                    .into_py_any(py),
1914                #[cfg(feature = "pct_change")]
1915                IRFunctionExpr::PctChange => ("pct_change",).into_py_any(py),
1916                IRFunctionExpr::Interpolate(method) => (
1917                    "interpolate",
1918                    match method {
1919                        InterpolationMethod::Linear => "linear",
1920                        InterpolationMethod::Nearest => "nearest",
1921                    },
1922                )
1923                    .into_py_any(py),
1924                IRFunctionExpr::InterpolateBy => ("interpolate_by",).into_py_any(py),
1925                IRFunctionExpr::Entropy { base, normalize } => {
1926                    ("entropy", base, normalize).into_py_any(py)
1927                },
1928                IRFunctionExpr::Log => ("log",).into_py_any(py),
1929                IRFunctionExpr::Log1p => ("log1p",).into_py_any(py),
1930                IRFunctionExpr::Exp => ("exp",).into_py_any(py),
1931                IRFunctionExpr::Unique(maintain_order) => {
1932                    ("unique", maintain_order).into_py_any(py)
1933                },
1934                IRFunctionExpr::Round { decimals, mode } => {
1935                    ("round", decimals, Into::<&str>::into(mode)).into_py_any(py)
1936                },
1937                IRFunctionExpr::RoundSF { digits } => ("round_sig_figs", digits).into_py_any(py),
1938                IRFunctionExpr::Truncate { decimals } => ("truncate", decimals).into_py_any(py),
1939                IRFunctionExpr::Floor => ("floor",).into_py_any(py),
1940                IRFunctionExpr::Ceil => ("ceil",).into_py_any(py),
1941                IRFunctionExpr::Fused(op) => {
1942                    let op_name = match op {
1943                        FusedOperator::MultiplyAdd => "fma",
1944                        FusedOperator::SubMultiply => "fsm",
1945                        FusedOperator::MultiplySub => "fms",
1946                    };
1947                    ("fused", op_name).into_py_any(py)
1948                },
1949                IRFunctionExpr::ConcatExpr { rechunk } => ("concat", rechunk).into_py_any(py),
1950                IRFunctionExpr::Correlation { method } => match method {
1951                    IRCorrelationMethod::Pearson => ("corr", "pearson").into_py_any(py),
1952                    IRCorrelationMethod::SpearmanRank(propagate_nans) => {
1953                        ("corr", "spearman_rank", propagate_nans).into_py_any(py)
1954                    },
1955                    IRCorrelationMethod::Covariance(ddof) => {
1956                        ("corr", "covariance", ddof).into_py_any(py)
1957                    },
1958                },
1959                #[cfg(feature = "peaks")]
1960                IRFunctionExpr::PeakMin => ("peak_max",).into_py_any(py),
1961                #[cfg(feature = "peaks")]
1962                IRFunctionExpr::PeakMax => ("peak_min",).into_py_any(py),
1963                #[cfg(feature = "cutqcut")]
1964                IRFunctionExpr::Cut {
1965                    breaks,
1966                    labels,
1967                    left_closed,
1968                    include_breaks,
1969                } => (
1970                    "cut",
1971                    breaks,
1972                    labels
1973                        .as_ref()
1974                        .map(|l| l.iter().map(|s| s.as_str()).collect::<Vec<_>>()),
1975                    left_closed,
1976                    include_breaks,
1977                )
1978                    .into_py_any(py),
1979                #[cfg(feature = "cutqcut")]
1980                IRFunctionExpr::QCut {
1981                    probs,
1982                    labels,
1983                    left_closed,
1984                    allow_duplicates,
1985                    include_breaks,
1986                } => (
1987                    "qcut",
1988                    probs,
1989                    labels
1990                        .as_ref()
1991                        .map(|l| l.iter().map(|s| s.as_str()).collect::<Vec<_>>()),
1992                    left_closed,
1993                    allow_duplicates,
1994                    include_breaks,
1995                )
1996                    .into_py_any(py),
1997                #[cfg(feature = "rle")]
1998                IRFunctionExpr::RLE => ("rle",).into_py_any(py),
1999                #[cfg(feature = "rle")]
2000                IRFunctionExpr::RLEID => ("rle_id",).into_py_any(py),
2001                IRFunctionExpr::ToPhysical => ("to_physical",).into_py_any(py),
2002                IRFunctionExpr::Random { method, seed } => match method {
2003                    IRRandomMethod::Shuffle => ("shuffle", seed).into_py_any(py),
2004                    IRRandomMethod::Sample {
2005                        is_fraction,
2006                        with_replacement,
2007                        shuffle,
2008                    } => ("sample", is_fraction, with_replacement, shuffle, seed).into_py_any(py),
2009                },
2010                IRFunctionExpr::SetSortedFlag(sorted) => {
2011                    ("set_sorted", sorted.descending, sorted.nulls_last).into_py_any(py)
2012                },
2013                #[cfg(feature = "ffi_plugin")]
2014                IRFunctionExpr::FfiPlugin {
2015                    flags,
2016                    lib,
2017                    symbol,
2018                    kwargs,
2019                } => (
2020                    "ffi_plugin",
2021                    lib.as_str(),
2022                    symbol.as_str(),
2023                    PyBytes::new(py, kwargs.as_ref()),
2024                    flags.is_elementwise(),
2025                )
2026                    .into_py_any(py),
2027                IRFunctionExpr::FoldHorizontal { .. } => {
2028                    Err(PyNotImplementedError::new_err("fold"))
2029                },
2030                IRFunctionExpr::ReduceHorizontal { .. } => {
2031                    Err(PyNotImplementedError::new_err("reduce"))
2032                },
2033                IRFunctionExpr::CumReduceHorizontal { .. } => {
2034                    Err(PyNotImplementedError::new_err("cum_reduce"))
2035                },
2036                IRFunctionExpr::CumFoldHorizontal { .. } => {
2037                    Err(PyNotImplementedError::new_err("cum_fold"))
2038                },
2039                IRFunctionExpr::SumHorizontal { ignore_nulls } => {
2040                    ("sum_horizontal", ignore_nulls).into_py_any(py)
2041                },
2042                IRFunctionExpr::MaxHorizontal => ("max_horizontal",).into_py_any(py),
2043                IRFunctionExpr::MeanHorizontal { ignore_nulls } => {
2044                    ("mean_horizontal", ignore_nulls).into_py_any(py)
2045                },
2046                IRFunctionExpr::MinHorizontal => ("min_horizontal",).into_py_any(py),
2047                // Tuples consumed by external engines (e.g., cudf-polars):
2048                // Mean/Std/Var: (EwmFunction, alpha, adjust, bias, min_periods, ignore_nulls)
2049                // Sum: (EwmFunction, alpha, min_periods, ignore_nulls)
2050                IRFunctionExpr::EwmMean { options } => (
2051                    PyEwmFunction::Mean,
2052                    options.alpha,
2053                    options.adjust,
2054                    options.bias,
2055                    options.min_periods,
2056                    options.ignore_nulls,
2057                )
2058                    .into_py_any(py),
2059                IRFunctionExpr::EwmSum { options } => (
2060                    PyEwmFunction::Sum,
2061                    options.alpha,
2062                    options.min_periods,
2063                    options.ignore_nulls,
2064                )
2065                    .into_py_any(py),
2066                IRFunctionExpr::EwmStd { options } => (
2067                    PyEwmFunction::Std,
2068                    options.alpha,
2069                    options.adjust,
2070                    options.bias,
2071                    options.min_periods,
2072                    options.ignore_nulls,
2073                )
2074                    .into_py_any(py),
2075                IRFunctionExpr::EwmVar { options } => (
2076                    PyEwmFunction::Var,
2077                    options.alpha,
2078                    options.adjust,
2079                    options.bias,
2080                    options.min_periods,
2081                    options.ignore_nulls,
2082                )
2083                    .into_py_any(py),
2084                IRFunctionExpr::Replace => ("replace",).into_py_any(py),
2085                IRFunctionExpr::ReplaceStrict { return_dtype: _ } => {
2086                    // Can ignore the return dtype because it is encoded in the schema.
2087                    ("replace_strict",).into_py_any(py)
2088                },
2089                IRFunctionExpr::Negate => ("negate",).into_py_any(py),
2090                IRFunctionExpr::FillNullWithStrategy(strategy) => {
2091                    let (strategy_str, py_limit): (&str, Py<PyAny>) = match strategy {
2092                        FillNullStrategy::Forward(limit) => {
2093                            let py_limit = limit
2094                                .map(|v| PyInt::new(py, v).into())
2095                                .unwrap_or_else(|| py.None());
2096                            ("forward", py_limit)
2097                        },
2098                        FillNullStrategy::Backward(limit) => {
2099                            let py_limit = limit
2100                                .map(|v| PyInt::new(py, v).into())
2101                                .unwrap_or_else(|| py.None());
2102                            ("backward", py_limit)
2103                        },
2104                        FillNullStrategy::Min => ("min", py.None()),
2105                        FillNullStrategy::Max => ("max", py.None()),
2106                        FillNullStrategy::Mean => ("mean", py.None()),
2107                        FillNullStrategy::Zero => ("zero", py.None()),
2108                        FillNullStrategy::One => ("one", py.None()),
2109                    };
2110
2111                    ("fill_null_with_strategy", strategy_str, py_limit).into_py_any(py)
2112                },
2113                IRFunctionExpr::GatherEvery { n, offset } => {
2114                    ("gather_every", offset, n).into_py_any(py)
2115                },
2116                IRFunctionExpr::Reinterpret(dtype) => {
2117                    ("reinterpret", &Wrap(dtype.clone())).into_py_any(py)
2118                },
2119                IRFunctionExpr::ExtendConstant => ("extend_constant",).into_py_any(py),
2120                IRFunctionExpr::Business(_) => {
2121                    return Err(PyNotImplementedError::new_err("business"));
2122                },
2123                #[cfg(feature = "top_k")]
2124                IRFunctionExpr::TopKBy { descending } => ("top_k_by", descending).into_py_any(py),
2125                IRFunctionExpr::EwmMeanBy { half_life } => {
2126                    (PyEwmFunction::MeanBy, Wrap(*half_life)).into_py_any(py)
2127                },
2128                IRFunctionExpr::EwmSumBy { half_life } => {
2129                    (PyEwmFunction::SumBy, Wrap(*half_life)).into_py_any(py)
2130                },
2131                IRFunctionExpr::RowEncode(..) => {
2132                    return Err(PyNotImplementedError::new_err("row_encode"));
2133                },
2134                IRFunctionExpr::RowDecode(..) => {
2135                    return Err(PyNotImplementedError::new_err("row_decode"));
2136                },
2137                IRFunctionExpr::DynamicPred { pred } => {
2138                    ("dynamic_pred", pred.id().map(|u| u.as_u128())).into_py_any(py)
2139                },
2140            }?,
2141            options: py.None(),
2142        }
2143        .into_py_any(py),
2144        AExpr::Rolling {
2145            function,
2146            index_column,
2147            period,
2148            offset,
2149            closed_window,
2150        } => Rolling {
2151            function: function.0,
2152            index_column: index_column.0,
2153            period: Wrap(*period).into_py_any(py)?,
2154            offset: Wrap(*offset).into_py_any(py)?,
2155            closed_window: Wrap(*closed_window).into_py_any(py)?,
2156        }
2157        .into_py_any(py),
2158        AExpr::Over {
2159            function,
2160            partition_by,
2161            order_by,
2162            mapping,
2163        } => {
2164            let function = function.0;
2165            let partition_by = partition_by.iter().map(|n| n.0).collect();
2166            let order_by_descending = order_by
2167                .map(|(_, options)| options.descending)
2168                .unwrap_or(false);
2169            let order_by_nulls_last = order_by
2170                .map(|(_, options)| options.nulls_last)
2171                .unwrap_or(false);
2172            let order_by = order_by.map(|(n, _)| n.0);
2173
2174            let options = PyWindowMapping { inner: *mapping }.into_py_any(py)?;
2175            Window {
2176                function,
2177                partition_by,
2178                order_by,
2179                order_by_descending,
2180                order_by_nulls_last,
2181                options,
2182            }
2183            .into_py_any(py)
2184        },
2185        AExpr::Slice {
2186            input,
2187            offset,
2188            length,
2189        } => Slice {
2190            input: input.0,
2191            offset: offset.0,
2192            length: length.0,
2193        }
2194        .into_py_any(py),
2195        AExpr::Len => Len {}.into_py_any(py),
2196        AExpr::Eval { .. } => Err(PyNotImplementedError::new_err("list.eval")),
2197        AExpr::StructEval { expr, evaluation } => StructEval {
2198            expr: expr.0,
2199            evaluation: evaluation.iter().map(|e| e.into()).collect(),
2200        }
2201        .into_py_any(py),
2202    }
2203}