1"""
2Routines for casting.
3"""
4
5from __future__ import annotations
6
7import datetime as dt
8import functools
9from typing import (
10 TYPE_CHECKING,
11 Any,
12 Literal,
13 TypeVar,
14 cast,
15 overload,
16)
17import warnings
18
19import numpy as np
20
21from pandas._config import (
22 is_nan_na,
23 using_python_scalars,
24 using_string_dtype,
25)
26
27from pandas._libs import (
28 Interval,
29 Period,
30 lib,
31)
32from pandas._libs.missing import (
33 NA,
34 NAType,
35 checknull,
36)
37from pandas._libs.tslibs import (
38 NaT,
39 OutOfBoundsDatetime,
40 OutOfBoundsTimedelta,
41 Timedelta,
42 Timestamp,
43 is_supported_dtype,
44)
45from pandas._libs.tslibs.timedeltas import array_to_timedelta64
46from pandas.errors import (
47 IntCastingNaNError,
48 LossySetitemError,
49)
50
51from pandas.core.dtypes.common import (
52 ensure_int8,
53 ensure_int16,
54 ensure_int32,
55 ensure_int64,
56 ensure_object,
57 ensure_str,
58 is_bool,
59 is_complex,
60 is_float,
61 is_integer,
62 is_object_dtype,
63 is_scalar,
64 is_string_dtype,
65 pandas_dtype as pandas_dtype_func,
66)
67from pandas.core.dtypes.dtypes import (
68 ArrowDtype,
69 BaseMaskedDtype,
70 CategoricalDtype,
71 DatetimeTZDtype,
72 ExtensionDtype,
73 IntervalDtype,
74 PandasExtensionDtype,
75 PeriodDtype,
76)
77from pandas.core.dtypes.generic import (
78 ABCExtensionArray,
79 ABCIndex,
80 ABCSeries,
81)
82from pandas.core.dtypes.inference import is_list_like
83from pandas.core.dtypes.missing import (
84 is_valid_na_for_dtype,
85 isna,
86 na_value_for_dtype,
87 notna,
88)
89
90from pandas.io._util import _arrow_dtype_mapping
91
92if TYPE_CHECKING:
93 from collections.abc import (
94 Collection,
95 Sequence,
96 )
97
98 from pandas._typing import (
99 ArrayLike,
100 Dtype,
101 DtypeObj,
102 NumpyIndexT,
103 Scalar,
104 TimeUnit,
105 )
106
107 from pandas import Index
108 from pandas.core.arrays import (
109 Categorical,
110 DatetimeArray,
111 ExtensionArray,
112 IntervalArray,
113 PeriodArray,
114 TimedeltaArray,
115 )
116
117
118_int8_max = np.iinfo(np.int8).max
119_int16_max = np.iinfo(np.int16).max
120_int32_max = np.iinfo(np.int32).max
121
122_dtype_obj = np.dtype(object)
123
124NumpyArrayT = TypeVar("NumpyArrayT", bound=np.ndarray)
125
126
127def is_nested_object(obj: object) -> bool:
128 """
129 return a boolean if we have a nested object, e.g. a Series with 1 or
130 more Series elements
131
132 This may not be necessarily be performant.
133
134 """
135 return bool(
136 isinstance(obj, ABCSeries)
137 and is_object_dtype(obj.dtype)
138 and any(isinstance(v, ABCSeries) for v in obj._values)
139 )
140
141
142def maybe_box_datetimelike(value: Scalar, dtype: Dtype | None = None) -> Scalar:
143 """
144 Cast scalar to Timestamp or Timedelta if scalar is datetime-like
145 and dtype is not object.
146
147 Parameters
148 ----------
149 value : scalar
150 dtype : Dtype, optional
151
152 Returns
153 -------
154 scalar
155 """
156 if dtype == _dtype_obj:
157 pass
158 elif isinstance(value, (np.datetime64, dt.datetime)):
159 value = Timestamp(value)
160 elif isinstance(value, (np.timedelta64, dt.timedelta)):
161 value = Timedelta(value)
162
163 return value
164
165
166def maybe_box_native(value: Scalar | NAType | None) -> Scalar | NAType | None:
167 """
168 If passed a scalar cast the scalar to a python native type.
169
170 Parameters
171 ----------
172 value : scalar or Series
173
174 Returns
175 -------
176 scalar or Series
177 """
178 if is_float(value):
179 value = float(value)
180 elif is_integer(value):
181 value = int(value)
182 elif is_bool(value):
183 value = bool(value)
184 elif isinstance(value, (np.datetime64, np.timedelta64)):
185 value = maybe_box_datetimelike(value)
186 elif value is NA:
187 value = None
188 return value
189
190
191def _maybe_unbox_datetimelike(value: Scalar, dtype: DtypeObj) -> Scalar:
192 """
193 Convert a Timedelta or Timestamp to timedelta64 or datetime64 for setting
194 into a numpy array. Failing to unbox would risk dropping nanoseconds.
195
196 Notes
197 -----
198 Caller is responsible for checking dtype.kind in "mM"
199 """
200 if is_valid_na_for_dtype(value, dtype):
201 # GH#36541: can't fill array directly with pd.NaT
202 # > np.empty(10, dtype="datetime64[ns]").fill(pd.NaT)
203 # ValueError: cannot convert float NaN to integer
204 value = dtype.type("NaT", "ns")
205 elif isinstance(value, Timestamp):
206 if value.tz is None:
207 value = value.to_datetime64()
208 elif not isinstance(dtype, DatetimeTZDtype):
209 raise TypeError("Cannot unbox tzaware Timestamp to tznaive dtype")
210 elif isinstance(value, Timedelta):
211 value = value.to_timedelta64()
212
213 _disallow_mismatched_datetimelike(value, dtype)
214 return value
215
216
217def _disallow_mismatched_datetimelike(value: Scalar, dtype: DtypeObj) -> None:
218 """
219 numpy allows np.array(dt64values, dtype="timedelta64[ns]") and
220 vice-versa, but we do not want to allow this, so we need to
221 check explicitly
222 """
223 vdtype = getattr(value, "dtype", None)
224 if vdtype is None:
225 return
226 elif (vdtype.kind == "m" and dtype.kind == "M") or (
227 vdtype.kind == "M" and dtype.kind == "m"
228 ):
229 raise TypeError(f"Cannot cast {value!r} to {dtype}")
230
231
232@overload
233def maybe_downcast_to_dtype(result: np.ndarray, dtype: np.dtype) -> np.ndarray: ...
234
235
236@overload
237def maybe_downcast_to_dtype(
238 result: ExtensionArray, dtype: np.dtype
239) -> ExtensionArray: ...
240
241
242def maybe_downcast_to_dtype(result: ArrayLike, dtype: np.dtype) -> ArrayLike:
243 """
244 try to cast to the specified dtype (e.g. convert back to bool/int
245 or could be an astype of float64->float32
246 """
247 if isinstance(result, ABCSeries):
248 result = result._values
249 do_round = False
250
251 if not isinstance(dtype, np.dtype):
252 # enforce our signature annotation
253 raise TypeError(dtype) # pragma: no cover
254
255 converted = maybe_downcast_numeric(result, dtype, do_round)
256 if converted is not result:
257 return converted
258
259 # a datetimelike
260 # GH12821, iNaT is cast to float
261 if dtype.kind in "mM" and result.dtype.kind in "if":
262 result = result.astype(dtype)
263
264 elif dtype.kind == "m" and result.dtype == _dtype_obj:
265 # test_where_downcast_to_td64
266 result = cast("np.ndarray", result)
267 result = array_to_timedelta64(result)
268
269 elif dtype == np.dtype("M8[ns]") and result.dtype == _dtype_obj:
270 result = cast("np.ndarray", result)
271 return np.asarray(maybe_cast_to_datetime(result, dtype=dtype))
272
273 return result
274
275
276def floats_fit_integer_dtype(values: np.ndarray, dtype: np.dtype) -> bool:
277 """
278 Whether every entry of float- or bool-dtype `values` is finite and within
279 the range that casts exactly to integer dtype `dtype`.
280
281 Casting a float outside that range to an integer dtype is undefined
282 behavior in C, so numpy's result varies by platform.
283 """
284 iinfo = np.iinfo(dtype)
285 # float(iinfo.max) rounds *up* for int64/uint64 (e.g. float64 has no
286 # 2**63 - 1), so compare against the exactly-representable power of two
287 # one past it, with a strict inequality.
288 lower = np.float64(iinfo.min)
289 upper = np.float64(int(iinfo.max) + 1)
290 # np.float64 scalars, unlike Python floats, promote a narrower `values`
291 # (e.g. float16) instead of downcasting the bounds and overflowing.
292 # NaN and +/-inf both fail these comparisons.
293 return bool(((values >= lower) & (values < upper)).all())
294
295
296@overload
297def maybe_downcast_numeric(
298 result: np.ndarray, dtype: np.dtype, do_round: bool = False
299) -> np.ndarray: ...
300
301
302@overload
303def maybe_downcast_numeric(
304 result: ExtensionArray, dtype: DtypeObj, do_round: bool = False
305) -> ArrayLike: ...
306
307
308def maybe_downcast_numeric(
309 result: ArrayLike, dtype: DtypeObj, do_round: bool = False
310) -> ArrayLike:
311 """
312 Subset of maybe_downcast_to_dtype restricted to numeric dtypes.
313
314 Parameters
315 ----------
316 result : ndarray or ExtensionArray
317 dtype : np.dtype or ExtensionDtype
318 do_round : bool
319
320 Returns
321 -------
322 ndarray or ExtensionArray
323 """
324 if not isinstance(dtype, np.dtype) or not isinstance(result.dtype, np.dtype):
325 # e.g. SparseDtype has no itemsize attr
326 return result
327
328 def trans(x: np.ndarray) -> np.ndarray:
329 if do_round:
330 return x.round()
331 return x
332
333 if dtype.kind == result.dtype.kind:
334 # don't allow upcasts here (except if empty)
335 if result.dtype.itemsize <= dtype.itemsize and result.size:
336 return result
337
338 if dtype.kind in "biu":
339 if not result.size:
340 # if we don't have any elements, just astype it
341 return trans(result).astype(dtype)
342
343 if isinstance(result, np.ndarray):
344 element = result.item(0)
345 else:
346 element = result.iloc[0]
347 if not isinstance(element, (np.integer, np.floating, int, float, bool)):
348 # a comparable, e.g. a Decimal may slip in here
349 return result
350
351 if (
352 issubclass(result.dtype.type, (np.object_, np.number))
353 and notna(result).all()
354 ):
355 new_result = trans(result)
356 if new_result.dtype.kind == "f" and dtype.kind in "iu":
357 # GH#66394 an out-of-range or non-finite float->int cast is
358 # platform-dependent (aarch64 saturates, x86 wraps), and the
359 # np.allclose check below cannot detect it, since e.g. 2**63
360 # and 2**63 - 1 are the same float64.
361 if not floats_fit_integer_dtype(new_result, dtype):
362 return result
363 try:
364 new_result = new_result.astype(dtype)
365 except (OverflowError, ValueError):
366 # GH#66394 object-dtype entries that don't fit `dtype`, e.g. a
367 # python int larger than int64 or an inf; not downcasting is
368 # the correct outcome, so don't let the error escape.
369 return result
370 if new_result.dtype.kind == "O" or result.dtype.kind == "O":
371 # np.allclose may raise TypeError on object-dtype
372 if (new_result == result).all():
373 return new_result
374 elif np.allclose(new_result, result, rtol=0):
375 return new_result
376
377 elif (
378 issubclass(dtype.type, np.floating)
379 and result.dtype.kind != "b"
380 and not is_string_dtype(result.dtype)
381 ):
382 with warnings.catch_warnings():
383 warnings.filterwarnings(
384 "ignore", "overflow encountered in cast", RuntimeWarning
385 )
386 new_result = result.astype(dtype)
387
388 # Adjust tolerances based on floating point size
389 size_tols = {4: 5e-4, 8: 5e-8, 16: 5e-16}
390
391 atol = size_tols.get(new_result.dtype.itemsize, 0.0)
392
393 # Check downcast float values are still equal within 7 digits when
394 # converting from float64 to float32
395 if np.allclose(new_result, result, equal_nan=True, rtol=0.0, atol=atol):
396 return new_result
397
398 elif dtype.kind == result.dtype.kind == "c":
399 new_result = result.astype(dtype)
400
401 if np.array_equal(new_result, result, equal_nan=True):
402 # TODO: use tolerance like we do for float?
403 return new_result
404
405 return result
406
407
408def maybe_upcast_numeric_to_64bit(arr: NumpyIndexT) -> NumpyIndexT:
409 """
410 If array is an int/uint/float bit size lower than 64 bit, upcast it to 64 bit.
411
412 Parameters
413 ----------
414 arr : ndarray or ExtensionArray
415
416 Returns
417 -------
418 ndarray or ExtensionArray
419 """
420 dtype = arr.dtype
421 if dtype.kind == "i" and dtype != np.int64:
422 return arr.astype(np.int64)
423 elif dtype.kind == "u" and dtype != np.uint64:
424 return arr.astype(np.uint64)
425 elif dtype.kind == "f" and dtype != np.float64:
426 return arr.astype(np.float64)
427 else:
428 return arr
429
430
431@overload
432def ensure_dtype_can_hold_na(dtype: np.dtype) -> np.dtype: ...
433
434
435@overload
436def ensure_dtype_can_hold_na(dtype: ExtensionDtype) -> ExtensionDtype: ...
437
438
439def ensure_dtype_can_hold_na(dtype: DtypeObj) -> DtypeObj:
440 """
441 If we have a dtype that cannot hold NA values, find the best match that can.
442 """
443 if isinstance(dtype, ExtensionDtype):
444 if dtype._can_hold_na:
445 return dtype
446 elif isinstance(dtype, IntervalDtype):
447 # TODO(GH#45349): don't special-case IntervalDtype, allow
448 # overriding instead of returning object below.
449 return IntervalDtype(np.float64, closed=dtype.closed)
450 return _dtype_obj
451 elif dtype.kind in "bSUV":
452 return _dtype_obj
453 elif dtype.kind in "iu":
454 return np.dtype(np.float64)
455 return dtype
456
457
458_canonical_nans = {
459 np.datetime64: np.datetime64("NaT", "ns"),
460 np.timedelta64: np.timedelta64("NaT", "ns"),
461 type(np.nan): np.nan,
462}
463
464
465def maybe_promote(dtype: np.dtype, fill_value: Any = np.nan) -> tuple[np.dtype, Any]:
466 """
467 Find the minimal dtype that can hold both the given dtype and fill_value.
468
469 Parameters
470 ----------
471 dtype : np.dtype
472 fill_value : scalar, default np.nan
473
474 Returns
475 -------
476 dtype
477 Upcasted from dtype argument if necessary.
478 fill_value
479 Upcasted from fill_value argument if necessary.
480
481 Raises
482 ------
483 ValueError
484 If fill_value is a non-scalar and dtype is not object.
485 """
486 orig = fill_value
487 orig_is_nat = False
488 if checknull(fill_value):
489 # https://github.com/pandas-dev/pandas/pull/39692#issuecomment-1441051740
490 # avoid cache misses with NaN/NaT values that are not singletons
491 if fill_value is not NA:
492 try:
493 orig_is_nat = np.isnat(fill_value)
494 except TypeError:
495 pass
496
497 fill_value = _canonical_nans.get(type(fill_value), fill_value)
498
499 # for performance, we are using a cached version of the actual implementation
500 # of the function in _maybe_promote. However, this doesn't always work (in case
501 # of non-hashable arguments), so we fallback to the actual implementation if needed
502 try:
503 # error: Argument 3 to "__call__" of "_lru_cache_wrapper" has incompatible type
504 # "Type[Any]"; expected "Hashable" [arg-type]
505 dtype, fill_value = _maybe_promote_cached(
506 dtype,
507 fill_value,
508 type(fill_value), # type: ignore[arg-type]
509 )
510 except TypeError:
511 # if fill_value is not hashable (required for caching)
512 dtype, fill_value = _maybe_promote(dtype, fill_value)
513
514 if (dtype == _dtype_obj and orig is not None) or (
515 orig_is_nat and np.datetime_data(orig)[0] != "ns"
516 ):
517 # GH#51592,53497 restore our potentially non-canonical fill_value
518 fill_value = orig
519 return dtype, fill_value
520
521
522@functools.lru_cache
523def _maybe_promote_cached(
524 dtype: np.dtype, fill_value: Any, fill_value_type: type
525) -> tuple[np.dtype, Any]:
526 # The cached version of _maybe_promote below
527 # This also use fill_value_type as (unused) argument to use this in the
528 # cache lookup -> to differentiate 1 and True
529 return _maybe_promote(dtype, fill_value)
530
531
532def _maybe_promote(dtype: np.dtype, fill_value: Any = np.nan) -> tuple[np.dtype, Any]:
533 # The actual implementation of the function, use `maybe_promote` above for
534 # a cached version.
535 if not is_scalar(fill_value):
536 # with object dtype there is nothing to promote, and the user can
537 # pass pretty much any weird fill_value they like
538 if dtype != object:
539 # with object dtype there is nothing to promote, and the user can
540 # pass pretty much any weird fill_value they like
541 raise ValueError("fill_value must be a scalar")
542 dtype = _dtype_obj
543 return dtype, fill_value
544
545 if is_valid_na_for_dtype(fill_value, dtype) and dtype.kind in "iufcmM":
546 dtype = ensure_dtype_can_hold_na(dtype)
547 fv = na_value_for_dtype(dtype)
548 return dtype, fv
549
550 elif isinstance(dtype, CategoricalDtype):
551 if fill_value in dtype.categories or isna(fill_value):
552 return dtype, fill_value
553 else:
554 return object, ensure_object(fill_value)
555
556 elif isna(fill_value):
557 dtype = _dtype_obj
558 if fill_value is None:
559 # but we retain e.g. pd.NA
560 fill_value = np.nan
561 return dtype, fill_value
562
563 # returns tuple of (dtype, fill_value)
564 if issubclass(dtype.type, np.datetime64):
565 inferred, fv = infer_dtype_from_scalar(fill_value)
566 if inferred == dtype:
567 return dtype, fv
568
569 from pandas.core.arrays import DatetimeArray
570
571 dta = DatetimeArray._from_sequence([], dtype="M8[ns]")
572 try:
573 fv = dta._validate_setitem_value(fill_value) # type: ignore[no-untyped-call]
574 return cast("np.dtype", dta.dtype), fv
575 except (ValueError, TypeError):
576 return _dtype_obj, fill_value
577
578 elif issubclass(dtype.type, np.timedelta64):
579 inferred, fv = infer_dtype_from_scalar(fill_value)
580 if inferred == dtype:
581 return dtype, fv
582
583 elif inferred.kind == "m":
584 # different unit, e.g. passed np.timedelta64(24, "h") with dtype=m8[ns]
585 # see if we can losslessly cast it to our dtype
586 unit = np.datetime_data(dtype)[0]
587 unit = cast("TimeUnit", unit)
588 try:
589 td = Timedelta(fill_value).as_unit(unit, round_ok=False)
590 except OutOfBoundsTimedelta:
591 return _dtype_obj, fill_value
592 else:
593 return dtype, td.asm8
594
595 return _dtype_obj, fill_value
596
597 elif is_float(fill_value):
598 if issubclass(dtype.type, np.bool_):
599 dtype = np.dtype(np.object_)
600
601 elif issubclass(dtype.type, np.integer):
602 dtype = np.dtype(np.float64)
603
604 elif dtype.kind == "f":
605 mst = np.min_scalar_type(fill_value)
606 if mst > dtype:
607 # e.g. mst is np.float64 and dtype is np.float32
608 dtype = mst
609
610 elif dtype.kind == "c":
611 mst = np.min_scalar_type(fill_value)
612 dtype = np.promote_types(dtype, mst)
613
614 elif is_bool(fill_value):
615 if not issubclass(dtype.type, np.bool_):
616 dtype = np.dtype(np.object_)
617
618 elif is_integer(fill_value):
619 if issubclass(dtype.type, np.bool_):
620 dtype = np.dtype(np.object_)
621
622 elif issubclass(dtype.type, np.integer):
623 if not np_can_cast_scalar(fill_value, dtype):
624 # upcast to prevent overflow
625 mst = np.min_scalar_type(fill_value)
626 dtype = np.promote_types(dtype, mst)
627 if dtype.kind == "f":
628 # Case where we disagree with numpy
629 dtype = np.dtype(np.object_)
630
631 elif is_complex(fill_value):
632 if issubclass(dtype.type, np.bool_):
633 dtype = np.dtype(np.object_)
634
635 elif issubclass(dtype.type, (np.integer, np.floating)):
636 mst = np.min_scalar_type(fill_value)
637 dtype = np.promote_types(dtype, mst)
638
639 elif dtype.kind == "c":
640 mst = np.min_scalar_type(fill_value)
641 if mst > dtype:
642 # e.g. mst is np.complex128 and dtype is np.complex64
643 dtype = mst
644
645 else:
646 dtype = np.dtype(np.object_)
647
648 # in case we have a string that looked like a number
649 if issubclass(dtype.type, (bytes, str)):
650 dtype = np.dtype(np.object_)
651
652 fill_value = _ensure_dtype_type(fill_value, dtype)
653 return dtype, fill_value
654
655
656def _ensure_dtype_type(value: Any, dtype: np.dtype) -> Any:
657 """
658 Ensure that the given value is an instance of the given dtype.
659
660 e.g. if out dtype is np.complex64_, we should have an instance of that
661 as opposed to a python complex object.
662
663 Parameters
664 ----------
665 value : object
666 dtype : np.dtype
667
668 Returns
669 -------
670 object
671 """
672 # Start with exceptions in which we do _not_ cast to numpy types
673
674 if dtype == _dtype_obj:
675 return value
676
677 # Note: before we get here we have already excluded isna(value)
678 return dtype.type(value)
679
680
681def infer_dtype_from(val: object) -> tuple[DtypeObj, Any]:
682 """
683 Interpret the dtype from a scalar or array.
684
685 Parameters
686 ----------
687 val : object
688 """
689 if not is_list_like(val):
690 return infer_dtype_from_scalar(val)
691 return infer_dtype_from_array(val)
692
693
694def infer_dtype_from_scalar(val: object) -> tuple[DtypeObj, Any]:
695 """
696 Interpret the dtype from a scalar.
697
698 Parameters
699 ----------
700 val : object
701 """
702 dtype: DtypeObj = _dtype_obj
703
704 # a 1-element ndarray
705 if isinstance(val, np.ndarray):
706 if val.ndim != 0:
707 msg = "invalid ndarray passed to infer_dtype_from_scalar"
708 raise ValueError(msg)
709
710 dtype = val.dtype
711 val = lib.item_from_zerodim(val)
712
713 elif isinstance(val, str):
714 # If we create an empty array using a string to infer
715 # the dtype, NumPy will only allocate one character per entry
716 # so this is kind of bad. Alternately we could use np.repeat
717 # instead of np.empty (but then you still don't want things
718 # coming out as np.str_!
719
720 dtype = _dtype_obj
721 if using_string_dtype():
722 from pandas.core.arrays.string_ import StringDtype
723
724 dtype = StringDtype(na_value=np.nan)
725
726 elif isinstance(val, (np.datetime64, dt.datetime)):
727 try:
728 val = Timestamp(val)
729 except OutOfBoundsDatetime:
730 return _dtype_obj, val
731
732 # error: Non-overlapping identity check (left operand type: "Timestamp",
733 # right operand type: "NaTType")
734 if val is NaT or val.tz is None: # type: ignore[comparison-overlap]
735 val = val.to_datetime64()
736 dtype = val.dtype
737 # TODO: test with datetime(2920, 10, 1) based on test_replace_dtypes
738 else:
739 dtype = DatetimeTZDtype(unit=val.unit, tz=val.tz)
740
741 elif isinstance(val, (np.timedelta64, dt.timedelta)):
742 try:
743 val = Timedelta(val)
744 except (OutOfBoundsTimedelta, OverflowError):
745 dtype = _dtype_obj
746 else:
747 # error: Non-overlapping identity check (left operand type: "Timedelta",
748 # right operand type: "NaTType")
749 if val is NaT: # type: ignore[comparison-overlap]
750 val = np.timedelta64("NaT", "ns")
751 else:
752 val = val.asm8
753 dtype = val.dtype
754
755 elif is_bool(val):
756 dtype = np.dtype(np.bool_)
757
758 elif is_integer(val):
759 if isinstance(val, np.integer):
760 dtype = np.dtype(type(val))
761 else:
762 dtype = np.dtype(np.int64)
763
764 try:
765 np.array(val, dtype=dtype)
766 except OverflowError:
767 dtype = np.array(val).dtype
768
769 elif is_float(val):
770 if isinstance(val, np.floating):
771 dtype = np.dtype(type(val))
772 else:
773 dtype = np.dtype(np.float64)
774
775 elif is_complex(val):
776 dtype = np.dtype(np.complex128)
777
778 if isinstance(val, Period):
779 dtype = PeriodDtype(freq=val.freq)
780 elif isinstance(val, Interval):
781 subtype = infer_dtype_from_scalar(val.left)[0]
782 dtype = IntervalDtype(subtype=subtype, closed=val.closed)
783
784 return dtype, val
785
786
787def dict_compat(d: dict[Scalar, Scalar]) -> dict[Scalar, Scalar]:
788 """
789 Convert datetimelike-keyed dicts to a Timestamp-keyed dict.
790
791 Parameters
792 ----------
793 d: dict-like object
794
795 Returns
796 -------
797 dict
798 """
799 return {maybe_box_datetimelike(key): value for key, value in d.items()}
800
801
802def infer_dtype_from_array(arr: Any) -> tuple[DtypeObj, ArrayLike]:
803 """
804 Infer the dtype from an array.
805
806 Parameters
807 ----------
808 arr : array
809
810 Returns
811 -------
812 tuple (pandas-compat dtype, array)
813
814
815 Examples
816 --------
817 >>> np.asarray([1, "1"])
818 array(['1', '1'], dtype='<U21')
819
820 >>> infer_dtype_from_array([1, "1"])
821 (dtype('O'), [1, '1'])
822 """
823 if isinstance(arr, np.ndarray):
824 return arr.dtype, arr
825
826 if not is_list_like(arr):
827 raise TypeError("'arr' must be list-like")
828
829 arr_dtype = getattr(arr, "dtype", None)
830 if isinstance(arr_dtype, ExtensionDtype):
831 return arr.dtype, arr
832
833 elif isinstance(arr, ABCSeries):
834 return arr.dtype, np.asarray(arr)
835
836 # don't force numpy coerce with nan's
837 inferred = lib.infer_dtype(arr, skipna=False)
838 if inferred in ["string", "bytes", "mixed", "mixed-integer"]:
839 return (np.dtype(np.object_), arr)
840
841 arr = np.asarray(arr)
842 return arr.dtype, arr
843
844
845def _maybe_infer_dtype_type(element: object) -> DtypeObj | None:
846 """
847 Try to infer an object's dtype, for use in arithmetic ops.
848
849 Uses `element.dtype` if that's available.
850 Objects implementing the iterator protocol are cast to a NumPy array,
851 and from there the array's type is used.
852
853 Parameters
854 ----------
855 element : object
856 Possibly has a `.dtype` attribute, and possibly the iterator
857 protocol.
858
859 Returns
860 -------
861 tipo : type
862
863 Examples
864 --------
865 >>> from collections import namedtuple
866 >>> Foo = namedtuple("Foo", "dtype")
867 >>> _maybe_infer_dtype_type(Foo(np.dtype("i8")))
868 dtype('int64')
869 """
870 tipo = None
871 if hasattr(element, "dtype"):
872 tipo = element.dtype
873 elif is_list_like(element):
874 element = np.asarray(element)
875 tipo = element.dtype
876 return tipo
877
878
879def coerce_indexer_dtype(indexer: np.ndarray, categories: Index) -> np.ndarray:
880 """coerce the indexer input array to the smallest dtype possible"""
881 length = len(categories)
882 if length < _int8_max:
883 return ensure_int8(indexer)
884 elif length < _int16_max:
885 return ensure_int16(indexer)
886 elif length < _int32_max:
887 return ensure_int32(indexer)
888 return ensure_int64(indexer)
889
890
891def convert_dtypes(
892 input_array: ArrayLike,
893 convert_string: bool = True,
894 convert_integer: bool = True,
895 convert_boolean: bool = True,
896 convert_floating: bool = True,
897 infer_objects: bool = False,
898 dtype_backend: Literal["numpy_nullable", "pyarrow"] = "numpy_nullable",
899) -> DtypeObj:
900 """
901 Convert objects to best possible type, and optionally,
902 to types supporting ``pd.NA``.
903
904 Parameters
905 ----------
906 input_array : ExtensionArray or np.ndarray
907 convert_string : bool, default True
908 Whether object dtypes should be converted to ``StringDtype()``.
909 convert_integer : bool, default True
910 Whether, if possible, conversion can be done to integer extension types.
911 convert_boolean : bool, defaults True
912 Whether object dtypes should be converted to ``BooleanDtypes()``.
913 convert_floating : bool, defaults True
914 Whether, if possible, conversion can be done to floating extension types.
915 If `convert_integer` is also True, preference will be give to integer
916 dtypes if the floats can be faithfully casted to integers.
917 infer_objects : bool, defaults False
918 Whether to also infer objects to float/int if possible. Is only hit if the
919 object array contains pd.NA.
920 dtype_backend : {'numpy_nullable', 'pyarrow'}, default 'numpy_nullable'
921 Back-end data type applied to the resultant :class:`DataFrame`
922 (still experimental). Behaviour is as follows:
923
924 * ``"numpy_nullable"``: returns nullable-dtype
925 * ``"pyarrow"``: returns pyarrow-backed nullable :class:`ArrowDtype`
926
927 .. versionadded:: 2.0
928
929 Returns
930 -------
931 np.dtype, or ExtensionDtype
932 """
933 from pandas.core.arrays.string_ import StringDtype
934
935 inferred_dtype: str | DtypeObj
936
937 if (
938 convert_string or convert_integer or convert_boolean or convert_floating
939 ) and isinstance(input_array, np.ndarray):
940 if input_array.dtype.kind == "c":
941 return input_array.dtype
942
943 if input_array.dtype == object:
944 inferred_dtype = lib.infer_dtype(input_array)
945 else:
946 inferred_dtype = input_array.dtype
947
948 if is_string_dtype(inferred_dtype):
949 if not convert_string or inferred_dtype == "bytes":
950 inferred_dtype = input_array.dtype
951 else:
952 inferred_dtype = pandas_dtype_func("string")
953
954 if convert_integer:
955 target_int_dtype = pandas_dtype_func("Int64")
956
957 if input_array.dtype.kind in "iu":
958 from pandas.core.arrays.integer import NUMPY_INT_TO_DTYPE
959
960 inferred_dtype = NUMPY_INT_TO_DTYPE.get(
961 input_array.dtype, target_int_dtype
962 )
963 elif input_array.dtype.kind in "fb":
964 arr = input_array[notna(input_array)]
965 if len(arr) < len(input_array) and not is_nan_na():
966 # In the presence of NaNs, we cannot convert to IntegerDtype
967 pass
968 elif (
969 floats_fit_integer_dtype(arr, np.dtype(int))
970 and (arr.astype(int) == arr).all()
971 ):
972 inferred_dtype = target_int_dtype
973 else:
974 inferred_dtype = input_array.dtype
975 elif (
976 infer_objects
977 and input_array.dtype == object
978 and (isinstance(inferred_dtype, str) and inferred_dtype == "integer")
979 ):
980 # GH#66517 the values need not fit in int64; ask for the dtype
981 # that can actually hold them and retain object if there is none
982 maybe_casted = lib.maybe_convert_objects(
983 input_array.ravel(), convert_to_nullable_dtype=True
984 )
985 if isinstance(maybe_casted.dtype, BaseMaskedDtype):
986 inferred_dtype = maybe_casted.dtype
987 else:
988 inferred_dtype = input_array.dtype
989
990 if convert_floating:
991 if input_array.dtype.kind in "fb":
992 # i.e. numeric but not integer
993 from pandas.core.arrays.floating import NUMPY_FLOAT_TO_DTYPE
994
995 inferred_float_dtype: DtypeObj = NUMPY_FLOAT_TO_DTYPE.get(
996 input_array.dtype, pandas_dtype_func("Float64")
997 )
998 # if we could also convert to integer, check if all floats
999 # are actually integers
1000 if convert_integer:
1001 arr = input_array[notna(input_array)]
1002 if len(arr) < len(input_array) and not is_nan_na():
1003 # In the presence of NaNs, we can't convert to IntegerDtype
1004 inferred_dtype = inferred_float_dtype
1005 elif (
1006 floats_fit_integer_dtype(arr, np.dtype(int))
1007 and (arr.astype(int) == arr).all()
1008 ):
1009 inferred_dtype = pandas_dtype_func("Int64")
1010 else:
1011 inferred_dtype = inferred_float_dtype
1012 else:
1013 inferred_dtype = inferred_float_dtype
1014 elif (
1015 infer_objects
1016 and input_array.dtype == object
1017 and (isinstance(inferred_dtype, str) and inferred_dtype == "floating")
1018 ):
1019 inferred_dtype = pandas_dtype_func("Float64")
1020
1021 if convert_boolean:
1022 if input_array.dtype.kind == "b":
1023 inferred_dtype = pandas_dtype_func("boolean")
1024 elif isinstance(inferred_dtype, str) and inferred_dtype == "boolean":
1025 inferred_dtype = pandas_dtype_func("boolean")
1026
1027 if isinstance(inferred_dtype, str):
1028 # If we couldn't do anything else, then we retain the dtype
1029 inferred_dtype = input_array.dtype
1030
1031 elif (
1032 convert_string
1033 and isinstance(input_array.dtype, StringDtype)
1034 and input_array.dtype.na_value is np.nan
1035 ):
1036 inferred_dtype = pandas_dtype_func("string")
1037
1038 else:
1039 inferred_dtype = input_array.dtype
1040
1041 if dtype_backend == "pyarrow" and not isinstance(inferred_dtype, ArrowDtype):
1042 from pandas.core.arrays.arrow.array import to_pyarrow_type
1043 from pandas.core.arrays.string_ import StringDtype
1044
1045 assert not isinstance(inferred_dtype, str)
1046
1047 if (
1048 (convert_integer and inferred_dtype.kind in "iu")
1049 or (convert_floating and inferred_dtype.kind in "f")
1050 or (convert_boolean and inferred_dtype.kind == "b")
1051 or (convert_string and isinstance(inferred_dtype, StringDtype))
1052 or (
1053 inferred_dtype.kind not in "iufb"
1054 and not isinstance(inferred_dtype, StringDtype)
1055 and not isinstance(inferred_dtype, CategoricalDtype)
1056 )
1057 ):
1058 if isinstance(inferred_dtype, PandasExtensionDtype) and not isinstance(
1059 inferred_dtype, DatetimeTZDtype
1060 ):
1061 base_dtype = inferred_dtype.base
1062 elif isinstance(inferred_dtype, (BaseMaskedDtype, ArrowDtype)):
1063 base_dtype = inferred_dtype.numpy_dtype
1064 elif isinstance(inferred_dtype, StringDtype):
1065 base_dtype = np.dtype(str)
1066 else:
1067 base_dtype = inferred_dtype
1068 if (
1069 base_dtype.kind == "O" # type: ignore[union-attr]
1070 and input_array.size > 0
1071 and isna(input_array).all()
1072 ):
1073 import pyarrow as pa
1074
1075 pa_type = pa.null()
1076 else:
1077 pa_type = to_pyarrow_type(base_dtype)
1078 if pa_type is not None:
1079 inferred_dtype = ArrowDtype(pa_type)
1080 elif dtype_backend == "numpy_nullable" and isinstance(inferred_dtype, ArrowDtype):
1081 # GH 53648
1082 inferred_dtype = _arrow_dtype_mapping()[inferred_dtype.pyarrow_dtype]
1083
1084 assert not isinstance(inferred_dtype, str)
1085 return inferred_dtype
1086
1087
1088def maybe_cast_to_datetime(
1089 value: np.ndarray | list[Any], dtype: np.dtype
1090) -> DatetimeArray | TimedeltaArray | np.ndarray:
1091 """
1092 try to cast the array/value to a datetimelike dtype, converting float
1093 nan to iNaT
1094
1095 Caller is responsible for handling ExtensionDtype cases and non dt64/td64
1096 cases.
1097 """
1098 from pandas.core.arrays.datetimes import DatetimeArray
1099 from pandas.core.arrays.timedeltas import TimedeltaArray
1100
1101 assert dtype.kind in "mM"
1102 if not is_list_like(value):
1103 raise TypeError("value must be listlike")
1104
1105 # TODO: _from_sequence would raise ValueError in cases where
1106 # _ensure_nanosecond_dtype raises TypeError
1107 _ensure_nanosecond_dtype(dtype)
1108
1109 if lib.is_np_dtype(dtype, "m"):
1110 res = TimedeltaArray._from_sequence(value, dtype=dtype)
1111 return res
1112 else:
1113 try:
1114 dta = DatetimeArray._from_sequence(value, dtype=dtype)
1115 except ValueError as err:
1116 # We can give a Series-specific exception message.
1117 if "cannot supply both a tz and a timezone-naive dtype" in str(err):
1118 raise ValueError(
1119 "Cannot convert timezone-aware data to "
1120 "timezone-naive dtype. Use "
1121 "pd.Series(values).dt.tz_localize(None) instead."
1122 ) from err
1123 raise
1124
1125 return dta
1126
1127
1128def _ensure_nanosecond_dtype(dtype: DtypeObj) -> None:
1129 """
1130 Convert dtypes with granularity less than nanosecond to nanosecond
1131
1132 >>> _ensure_nanosecond_dtype(np.dtype("M8[us]"))
1133
1134 >>> _ensure_nanosecond_dtype(np.dtype("M8[D]"))
1135 Traceback (most recent call last):
1136 ...
1137 TypeError: dtype=datetime64[D] is not supported. Supported resolutions are 's', 'ms', 'us', and 'ns'
1138
1139 >>> _ensure_nanosecond_dtype(np.dtype("m8[ps]"))
1140 Traceback (most recent call last):
1141 ...
1142 TypeError: dtype=timedelta64[ps] is not supported. Supported resolutions are 's', 'ms', 'us', and 'ns'
1143 """ # noqa: E501
1144 msg = (
1145 f"The '{dtype.name}' dtype has no unit. "
1146 f"Please pass in '{dtype.name}[ns]' instead."
1147 )
1148
1149 # unpack e.g. SparseDtype
1150 dtype = getattr(dtype, "subtype", dtype)
1151
1152 if not isinstance(dtype, np.dtype):
1153 # i.e. datetime64tz
1154 pass
1155
1156 elif dtype.kind in "mM":
1157 if not is_supported_dtype(dtype):
1158 # pre-2.0 we would silently swap in nanos for lower-resolutions,
1159 # raise for above-nano resolutions
1160 if dtype.name in ["datetime64", "timedelta64"]:
1161 raise ValueError(msg)
1162 # TODO: ValueError or TypeError? existing test
1163 # test_constructor_generic_timestamp_bad_frequency expects TypeError
1164 raise TypeError(
1165 f"dtype={dtype} is not supported. Supported resolutions are 's', "
1166 "'ms', 'us', and 'ns'"
1167 )
1168
1169
1170# TODO: other value-dependent functions to standardize here include
1171# Index._find_common_type_compat
1172def find_result_type(left_dtype: DtypeObj, right: Any) -> DtypeObj:
1173 """
1174 Find the type/dtype for the result of an operation between objects.
1175
1176 This is similar to find_common_type, but looks at the right object instead
1177 of just its dtype. This can be useful in particular when the right
1178 object does not have a `dtype`.
1179
1180 Parameters
1181 ----------
1182 left_dtype : np.dtype or ExtensionDtype
1183 right : Any
1184
1185 Returns
1186 -------
1187 np.dtype or ExtensionDtype
1188
1189 See also
1190 --------
1191 find_common_type
1192 numpy.result_type
1193 """
1194 new_dtype: DtypeObj
1195
1196 if (
1197 isinstance(left_dtype, np.dtype)
1198 and left_dtype.kind in "iuc"
1199 and (lib.is_integer(right) or lib.is_float(right))
1200 ):
1201 # e.g. with int8 dtype and right=512, we want to end up with
1202 # np.int16, whereas infer_dtype_from(512) gives np.int64,
1203 # which will make us upcast too far.
1204 if lib.is_float(right) and right.is_integer() and left_dtype.kind != "f":
1205 right = int(right)
1206 # After NEP 50, numpy won't inspect Python scalars
1207 # TODO: do we need to recreate numpy's inspection logic for floats too
1208 # (this breaks some tests)
1209 if isinstance(right, int) and not isinstance(right, np.integer):
1210 # This gives an unsigned type by default
1211 # (if our number is positive)
1212
1213 # If our left dtype is signed, we might not want this since
1214 # this might give us 1 dtype too big
1215 # We should check if the corresponding int dtype (e.g. int64 for uint64)
1216 # can hold the number
1217 right_dtype = np.min_scalar_type(right)
1218 if right == 0:
1219 # Special case 0
1220 right = left_dtype
1221 elif (
1222 not np.issubdtype(left_dtype, np.unsignedinteger)
1223 and 0 < right <= np.iinfo(right_dtype).max
1224 ):
1225 # If left dtype isn't unsigned, check if it fits in the signed dtype
1226 right = np.dtype(f"i{right_dtype.itemsize}")
1227 else:
1228 right = right_dtype
1229
1230 new_dtype = np.result_type(left_dtype, right)
1231
1232 elif is_valid_na_for_dtype(right, left_dtype):
1233 # e.g. IntervalDtype[int] and None/np.nan
1234 new_dtype = ensure_dtype_can_hold_na(left_dtype)
1235
1236 else:
1237 dtype, _ = infer_dtype_from(right)
1238 new_dtype = find_common_type([left_dtype, dtype])
1239
1240 return new_dtype
1241
1242
1243def common_dtype_categorical_compat(
1244 objs: Sequence[Index | ArrayLike], dtype: DtypeObj
1245) -> DtypeObj:
1246 """
1247 Update the result of find_common_type to account for NAs in a Categorical.
1248
1249 Parameters
1250 ----------
1251 objs : list[np.ndarray | ExtensionArray | Index]
1252 dtype : np.dtype or ExtensionDtype
1253
1254 Returns
1255 -------
1256 np.dtype or ExtensionDtype
1257 """
1258 # GH#38240
1259
1260 # TODO: more generally, could do `not can_hold_na(dtype)`
1261 if lib.is_np_dtype(dtype, "iu"):
1262 for obj in objs:
1263 # We don't want to accidentally allow e.g. "categorical" str here
1264 obj_dtype = getattr(obj, "dtype", None)
1265 if isinstance(obj_dtype, CategoricalDtype):
1266 if isinstance(obj, ABCIndex):
1267 # This check may already be cached
1268 hasnas = obj.hasnans
1269 else:
1270 # Categorical
1271 hasnas = cast("Categorical", obj)._hasna
1272
1273 if hasnas:
1274 # see test_union_int_categorical_with_nan
1275 dtype = np.dtype(np.float64)
1276 break
1277 return dtype
1278
1279
1280def np_find_common_type(*dtypes: np.dtype) -> np.dtype:
1281 """
1282 np.find_common_type implementation pre-1.25 deprecation using np.result_type
1283 https://github.com/pandas-dev/pandas/pull/49569#issuecomment-1308300065
1284
1285 Parameters
1286 ----------
1287 dtypes : np.dtypes
1288
1289 Returns
1290 -------
1291 np.dtype
1292 """
1293 try:
1294 common_dtype = np.result_type(*dtypes)
1295 if common_dtype.kind in "mMSU":
1296 # NumPy promotion currently (1.25) misbehaves for times and strings,
1297 # so fall back to object (find_common_dtype did unless there
1298 # was only one dtype)
1299 common_dtype = np.dtype("O")
1300
1301 except TypeError:
1302 common_dtype = np.dtype("O")
1303 return common_dtype
1304
1305
1306@overload
1307def find_common_type(types: list[np.dtype]) -> np.dtype: ...
1308
1309
1310@overload
1311def find_common_type(types: list[ExtensionDtype]) -> DtypeObj: ...
1312
1313
1314@overload
1315def find_common_type(types: list[DtypeObj]) -> DtypeObj: ...
1316
1317
1318def find_common_type(types: list[DtypeObj]) -> DtypeObj: # type: ignore[misc]
1319 """
1320 Find a common data type among the given dtypes.
1321
1322 Parameters
1323 ----------
1324 types : list of dtypes
1325
1326 Returns
1327 -------
1328 pandas extension or numpy dtype
1329
1330 See Also
1331 --------
1332 numpy.find_common_type
1333
1334 """
1335 if not types:
1336 raise ValueError("no types given")
1337
1338 first = types[0]
1339
1340 # workaround for find_common_type([np.dtype('datetime64[ns]')] * 2)
1341 # => object
1342 if lib.dtypes_all_equal(list(types)):
1343 return first
1344
1345 # get unique types (dict.fromkeys is used as order-preserving set())
1346 types = list(dict.fromkeys(types).keys())
1347
1348 if any(isinstance(t, ExtensionDtype) for t in types):
1349 for t in types:
1350 if isinstance(t, ExtensionDtype):
1351 res = t._get_common_dtype(types)
1352 if res is not None:
1353 return res
1354 return np.dtype("object")
1355
1356 # At this point, all types are np.dtype (ExtensionDtype was handled above)
1357 np_types = cast("list[np.dtype]", types)
1358
1359 # take lowest unit
1360 if all(lib.is_np_dtype(t, "M") for t in np_types):
1361 return np.dtype(max(np_types))
1362 if all(lib.is_np_dtype(t, "m") for t in np_types):
1363 return np.dtype(max(np_types))
1364
1365 # don't mix bool / int or float or complex
1366 # this is different from numpy, which casts bool with float/int as int
1367 has_bools = any(t.kind == "b" for t in np_types)
1368 if has_bools:
1369 for t in np_types:
1370 if t.kind in "iufc":
1371 return np.dtype("object")
1372
1373 return np_find_common_type(*np_types)
1374
1375
1376def construct_2d_arraylike_from_scalar(
1377 value: Scalar, length: int, width: int, dtype: np.dtype, copy: bool
1378) -> np.ndarray:
1379 shape = (length, width)
1380
1381 if dtype.kind in "mM":
1382 value = _maybe_box_and_unbox_datetimelike(value, dtype)
1383 elif dtype == _dtype_obj:
1384 if isinstance(value, (np.timedelta64, np.datetime64)):
1385 # calling np.array below would cast to pytimedelta/pydatetime
1386 out = np.empty(shape, dtype=object)
1387 out.fill(value)
1388 return out
1389
1390 # Attempt to coerce to a numpy array
1391 try:
1392 if not copy:
1393 arr = np.asarray(value, dtype=dtype)
1394 else:
1395 arr = np.array(value, dtype=dtype, copy=copy)
1396 except (ValueError, TypeError) as err:
1397 raise TypeError(
1398 f"DataFrame constructor called with incompatible data and dtype: {err}"
1399 ) from err
1400
1401 if arr.ndim != 0:
1402 raise ValueError("DataFrame constructor not properly called!")
1403
1404 return np.full(shape, arr)
1405
1406
1407def construct_1d_arraylike_from_scalar(
1408 value: Scalar, length: int, dtype: DtypeObj | None
1409) -> ArrayLike:
1410 """
1411 create an np.ndarray / pandas type of specified shape and dtype
1412 filled with values
1413
1414 Parameters
1415 ----------
1416 value : scalar value
1417 length : int
1418 dtype : pandas_dtype or np.dtype
1419
1420 Returns
1421 -------
1422 np.ndarray / pandas type of length, filled with value
1423
1424 """
1425
1426 if dtype is None:
1427 try:
1428 dtype, value = infer_dtype_from_scalar(value)
1429 except OutOfBoundsDatetime:
1430 dtype = _dtype_obj
1431
1432 if isinstance(dtype, ExtensionDtype):
1433 cls = dtype.construct_array_type()
1434 seq = [] if length == 0 else [value]
1435 return cls._from_sequence(seq, dtype=dtype).repeat(length)
1436
1437 if length and dtype.kind in "iu" and isna(value):
1438 # coerce if we have nan for an integer dtype
1439 dtype = np.dtype("float64")
1440 elif lib.is_np_dtype(dtype, "US"):
1441 # we need to coerce to object dtype to avoid
1442 # to allow numpy to take our string as a scalar value
1443 dtype = np.dtype("object")
1444 if not isna(value):
1445 value = ensure_str(value)
1446 elif dtype.kind in "mM":
1447 value = _maybe_box_and_unbox_datetimelike(value, dtype)
1448
1449 subarr = np.empty(length, dtype=dtype)
1450 if length:
1451 # GH 47391: numpy > 1.24 will raise filling np.nan into int dtypes
1452 subarr.fill(value)
1453
1454 return subarr
1455
1456
1457def maybe_unbox_numpy_scalar(value: Any, *, object_with_dtype: Any = None) -> Any:
1458 """
1459 Maybe convert a NumPy scalar to its Python equivalent.
1460
1461 If future.python_scalars is disabled or ``value`` is not a NumPy scalar, ``value``
1462 is returned unchanged. ``np.datetime64`` and ``np.timedelta64`` values are
1463 converted to ``Timestamp`` and ``Timedelta`` respectively. Converting
1464 ``np.longdouble`` to ``float`` and ``np.clongdouble`` to ``complex`` can
1465 lose precision.
1466
1467 Parameters
1468 ----------
1469 value : Any
1470 The value to unbox.
1471 object_with_dtype : object with a ``dtype`` attribute, optional
1472 The data (e.g. a Series or array) that ``value`` came from. Pass this
1473 whenever ``value`` is an element of the data or is derived from its
1474 elements: object dtype stores arbitrary user objects, so a NumPy
1475 scalar coming from object-dtype data is a stored value rather than a
1476 boxing artifact, and is returned unchanged. Omit for values whose
1477 type does not follow the data's dtype, e.g. positions, counts, and
1478 the results of any/all. The ``.dtype`` attribute is only accessed if
1479 unboxing is actually going to happen, so passing the object rather
1480 than its dtype avoids that cost on hot paths.
1481
1482 Returns
1483 -------
1484 Any
1485 The equivalent Python scalar, or ``value`` unchanged.
1486 """
1487 result = value
1488 if using_python_scalars() and isinstance(value, np.generic):
1489 if object_with_dtype is not None and object_with_dtype.dtype == object:
1490 return result
1491 if isinstance(result, np.longdouble):
1492 result = float(result)
1493 elif isinstance(result, np.clongdouble):
1494 result = complex(result)
1495 elif isinstance(result, np.datetime64):
1496 result = Timestamp(result)
1497 elif isinstance(result, np.timedelta64):
1498 result = Timedelta(result)
1499 else:
1500 result = value.item()
1501 return result
1502
1503
1504def _maybe_box_and_unbox_datetimelike(value: Scalar, dtype: DtypeObj) -> Scalar:
1505 # Caller is responsible for checking dtype.kind in "mM"
1506
1507 if isinstance(value, dt.datetime):
1508 # we dont want to box dt64, in particular datetime64("NaT")
1509 value = maybe_box_datetimelike(value, dtype)
1510
1511 return _maybe_unbox_datetimelike(value, dtype)
1512
1513
1514def construct_1d_object_array_from_listlike(values: Collection[Any]) -> np.ndarray:
1515 """
1516 Transform any list-like object in a 1-dimensional numpy array of object
1517 dtype.
1518
1519 Parameters
1520 ----------
1521 values : any iterable which has a len()
1522
1523 Raises
1524 ------
1525 TypeError
1526 * If `values` does not have a len()
1527
1528 Returns
1529 -------
1530 1-dimensional numpy array of dtype object
1531 """
1532 # numpy will try to interpret nested lists as further dimensions in np.array(),
1533 # hence explicitly making a 1D array using np.fromiter
1534 return np.fromiter(values, dtype="object", count=len(values))
1535
1536
1537def maybe_cast_to_integer_array(
1538 arr: list[Any] | np.ndarray, dtype: np.dtype
1539) -> np.ndarray:
1540 """
1541 Takes any dtype and returns the casted version, raising for when data is
1542 incompatible with integer/unsigned integer dtypes.
1543
1544 Parameters
1545 ----------
1546 arr : np.ndarray or list
1547 The array to cast.
1548 dtype : np.dtype
1549 The integer dtype to cast the array to.
1550
1551 Returns
1552 -------
1553 ndarray
1554 Array of integer or unsigned integer dtype.
1555
1556 Raises
1557 ------
1558 OverflowError : the dtype is incompatible with the data
1559 ValueError : loss of precision has occurred during casting
1560
1561 Examples
1562 --------
1563 If you try to coerce negative values to unsigned integers, it raises:
1564
1565 >>> pd.Series([-1], dtype="uint64")
1566 Traceback (most recent call last):
1567 ...
1568 OverflowError: Trying to coerce negative values to unsigned integers
1569
1570 Also, if you try to coerce float values to integers, it raises:
1571
1572 >>> maybe_cast_to_integer_array([1, 2, 3.5], dtype=np.dtype("int64"))
1573 Traceback (most recent call last):
1574 ...
1575 ValueError: Trying to coerce float values to integers
1576 """
1577 assert dtype.kind in "iu"
1578
1579 try:
1580 if not isinstance(arr, np.ndarray):
1581 with warnings.catch_warnings():
1582 # We already disallow dtype=uint w/ negative numbers
1583 # (test_constructor_coercion_signed_to_unsigned) so safe to ignore.
1584 warnings.filterwarnings(
1585 "ignore",
1586 "NumPy will stop allowing conversion of out-of-bound Python int",
1587 DeprecationWarning,
1588 )
1589 casted = np.asarray(arr, dtype=dtype)
1590 else:
1591 with warnings.catch_warnings():
1592 warnings.filterwarnings("ignore", category=RuntimeWarning)
1593 casted = arr.astype(dtype, copy=False)
1594 except OverflowError as err:
1595 raise OverflowError(
1596 "The elements provided in the data cannot all be "
1597 f"casted to the dtype {dtype}"
1598 ) from err
1599
1600 if isinstance(arr, np.ndarray) and arr.dtype == dtype:
1601 # avoid expensive array_equal check
1602 return casted
1603
1604 with warnings.catch_warnings():
1605 warnings.filterwarnings("ignore", category=RuntimeWarning)
1606 warnings.filterwarnings(
1607 "ignore", "elementwise comparison failed", FutureWarning
1608 )
1609 if np.array_equal(arr, casted):
1610 return casted
1611
1612 # We do this casting to allow for proper
1613 # data and dtype checking.
1614 #
1615 # We didn't do this earlier because NumPy
1616 # doesn't handle `uint64` correctly.
1617 arr = np.asarray(arr)
1618
1619 if np.issubdtype(arr.dtype, str):
1620 # TODO(numpy-2.0 min): This case will raise an OverflowError above
1621 if (casted.astype(str) == arr).all():
1622 return casted
1623 raise ValueError(f"string values cannot be losslessly cast to {dtype}")
1624
1625 if dtype.kind == "u" and (arr < 0).any():
1626 # TODO: can this be hit anymore after numpy 2.0?
1627 raise OverflowError("Trying to coerce negative values to unsigned integers")
1628
1629 if arr.dtype.kind == "f":
1630 if not np.isfinite(arr).all():
1631 raise IntCastingNaNError(
1632 "Cannot convert non-finite values (NA or inf) to integer"
1633 )
1634 raise ValueError("Trying to coerce float values to integers")
1635 if arr.dtype == object:
1636 raise ValueError("Trying to coerce object values to integers")
1637
1638 if casted.dtype < arr.dtype:
1639 # TODO: Can this path be hit anymore with numpy > 2
1640 # GH#41734 e.g. [1, 200, 923442] and dtype="int8" -> overflows
1641 raise ValueError(
1642 f"Values are too large to be losslessly converted to {dtype}. "
1643 f"To cast anyway, use pd.Series(values).astype({dtype})"
1644 )
1645
1646 if arr.dtype.kind in "mM":
1647 # test_constructor_maskedarray_nonfloat
1648 raise TypeError(
1649 f"Constructing a Series or DataFrame from {arr.dtype} values and "
1650 f"dtype={dtype} is not supported. Use values.view({dtype}) instead."
1651 )
1652
1653 # No known cases that get here, but raising explicitly to cover our bases.
1654 raise ValueError(f"values cannot be losslessly cast to {dtype}")
1655
1656
1657def can_hold_element(arr: ArrayLike, element: Any) -> bool:
1658 """
1659 Can we do an inplace setitem with this element in an array with this dtype?
1660
1661 Parameters
1662 ----------
1663 arr : np.ndarray or ExtensionArray
1664 element : Any
1665
1666 Returns
1667 -------
1668 bool
1669 """
1670 dtype = arr.dtype
1671 if not isinstance(dtype, np.dtype) or dtype.kind in "mM":
1672 if isinstance(dtype, (PeriodDtype, IntervalDtype, DatetimeTZDtype, np.dtype)):
1673 # np.dtype here catches datetime64ns and timedelta64ns; we assume
1674 # in this case that we have DatetimeArray/TimedeltaArray
1675 arr = cast(
1676 "PeriodArray | DatetimeArray | TimedeltaArray | IntervalArray", arr
1677 )
1678 try:
1679 arr._validate_setitem_value(element) # type: ignore[no-untyped-call]
1680 return True
1681 except (ValueError, TypeError):
1682 return False
1683
1684 if dtype == "string":
1685 try:
1686 arr._validate_setitem_value(element) # type: ignore[union-attr]
1687 return True
1688 except (ValueError, TypeError):
1689 return False
1690
1691 # This is technically incorrect, but maintains the behavior of
1692 # ExtensionBlock._can_hold_element
1693 return True
1694
1695 try:
1696 np_can_hold_element(dtype, element)
1697 return True
1698 except (TypeError, LossySetitemError):
1699 return False
1700
1701
1702def np_can_hold_element(dtype: np.dtype, element: Any) -> Any:
1703 """
1704 Raise if we cannot losslessly set this element into an ndarray with this dtype.
1705
1706 Specifically about places where we disagree with numpy. i.e. there are
1707 cases where numpy will raise in doing the setitem that we do not check
1708 for here, e.g. setting str "X" into a numeric ndarray.
1709
1710 Returns
1711 -------
1712 Any
1713 The element, potentially cast to the dtype.
1714
1715 Raises
1716 ------
1717 ValueError : If we cannot losslessly store this element with this dtype.
1718 """
1719 if dtype == _dtype_obj:
1720 return element
1721
1722 tipo = _maybe_infer_dtype_type(element)
1723
1724 if dtype.kind in "iu":
1725 if isinstance(element, range):
1726 if _dtype_can_hold_range(element, dtype):
1727 return element
1728 raise LossySetitemError
1729
1730 if is_integer(element) or (is_float(element) and element.is_integer()):
1731 # e.g. test_setitem_series_int8 if we have a python int 1
1732 # tipo may be np.int32, despite the fact that it will fit
1733 # in smaller int dtypes.
1734 info = np.iinfo(dtype)
1735 if info.min <= element <= info.max:
1736 return dtype.type(element)
1737 raise LossySetitemError
1738
1739 if tipo is not None:
1740 if tipo.kind not in "iu":
1741 if isinstance(element, np.ndarray) and element.dtype.kind == "f":
1742 # If all can be losslessly cast to integers, then we can hold them
1743 with np.errstate(invalid="ignore"):
1744 # We check afterwards if cast was losslessly, so no need to show
1745 # the warning
1746 casted = element.astype(dtype)
1747 comp = casted == element
1748 if comp.all():
1749 # Return the casted values bc they can be passed to
1750 # np.putmask, whereas the raw values cannot.
1751 # see TestSetitemFloatNDarrayIntoIntegerSeries
1752 return casted
1753 raise LossySetitemError
1754
1755 elif isinstance(element, ABCExtensionArray) and isinstance(
1756 element.dtype, CategoricalDtype
1757 ):
1758 # GH#52927 setting Categorical value into non-EA frame
1759 # TODO: general-case for EAs?
1760 try:
1761 casted = element.astype(dtype)
1762 except (ValueError, TypeError) as err:
1763 raise LossySetitemError from err
1764 # Check for cases of either
1765 # a) lossy overflow/rounding or
1766 # b) semantic changes like dt64->int64
1767 comp = casted == element
1768 if not comp.all():
1769 raise LossySetitemError
1770 return casted
1771
1772 # Anything other than integer we cannot hold
1773 raise LossySetitemError
1774 if (
1775 dtype.kind == "u"
1776 and isinstance(element, np.ndarray)
1777 and element.dtype.kind == "i"
1778 ):
1779 # see test_where_uint64
1780 casted = element.astype(dtype)
1781 if (casted == element).all():
1782 # TODO: faster to check (element >=0).all()? potential
1783 # itemsize issues there?
1784 return casted
1785 raise LossySetitemError
1786 if dtype.itemsize < tipo.itemsize: # type: ignore[union-attr]
1787 raise LossySetitemError
1788 if not isinstance(tipo, np.dtype):
1789 # i.e. nullable IntegerDtype; we can put this into an ndarray
1790 # losslessly iff it has no NAs and the values themselves fit
1791 arr = (
1792 element._values
1793 if isinstance(element, (ABCIndex, ABCSeries))
1794 else element
1795 )
1796 if arr._hasna: # type: ignore[union-attr]
1797 raise LossySetitemError
1798 # GH#47776 re-run the ndarray guards on the NA-free values, e.g.
1799 # to reject a negative value going into an unsigned dtype.
1800 np_can_hold_element(dtype, np.asarray(arr))
1801 return element
1802
1803 return element
1804
1805 raise LossySetitemError
1806
1807 if dtype.kind == "f":
1808 if lib.is_integer(element) or lib.is_float(element):
1809 casted = dtype.type(element)
1810 if np.isnan(casted) or casted == element:
1811 return casted
1812 # otherwise e.g. overflow see TestCoercionFloat32
1813 raise LossySetitemError
1814
1815 if tipo is not None:
1816 # TODO: itemsize check?
1817
1818 if isinstance(tipo, CategoricalDtype):
1819 # GH#56376
1820 if tipo.categories.dtype.kind not in "iuf":
1821 # Anything other than float/integer we cannot hold
1822 raise LossySetitemError
1823 casted = np.asarray(element, dtype=dtype)
1824 if np.array_equal(casted, element, equal_nan=True):
1825 return casted
1826 raise LossySetitemError
1827
1828 if tipo.kind not in "iuf":
1829 # Anything other than float/integer we cannot hold
1830 raise LossySetitemError
1831 if not isinstance(tipo, np.dtype):
1832 # i.e. nullable IntegerDtype or FloatingDtype;
1833 # we can put this into an ndarray losslessly iff it has no NAs
1834 # and the values themselves fit
1835 arr = (
1836 element._values
1837 if isinstance(element, (ABCIndex, ABCSeries))
1838 else element
1839 )
1840 if arr._hasna: # type: ignore[union-attr]
1841 raise LossySetitemError
1842 # GH#47776 re-run the ndarray guards on the NA-free values, e.g.
1843 # to reject a value that overflows the target float dtype.
1844 np_can_hold_element(dtype, np.asarray(arr))
1845 return element
1846 elif tipo.itemsize > dtype.itemsize or tipo.kind != dtype.kind:
1847 if isinstance(element, np.ndarray):
1848 # e.g. TestDataFrameIndexingWhere::test_where_alignment
1849 with np.errstate(over="ignore", invalid="ignore"):
1850 # losslessness is checked below, so cast warnings
1851 # (e.g. overflow) are spurious
1852 casted = element.astype(dtype)
1853 if np.array_equal(casted, element, equal_nan=True):
1854 return casted
1855 raise LossySetitemError
1856
1857 return element
1858
1859 raise LossySetitemError
1860
1861 if dtype.kind == "c":
1862 if lib.is_integer(element) or lib.is_complex(element) or lib.is_float(element):
1863 if np.isnan(element):
1864 # see test_where_complex GH#6345
1865 return dtype.type(element)
1866
1867 with warnings.catch_warnings():
1868 warnings.filterwarnings("ignore")
1869 casted = dtype.type(element)
1870 if casted == element:
1871 return casted
1872 # otherwise e.g. overflow see test_32878_complex_itemsize
1873 raise LossySetitemError
1874
1875 if tipo is not None:
1876 if tipo.kind in "iufc":
1877 return element
1878 raise LossySetitemError
1879 raise LossySetitemError
1880
1881 if dtype.kind == "b":
1882 if tipo is not None:
1883 if tipo.kind == "b":
1884 if not isinstance(tipo, np.dtype):
1885 # i.e. we have a BooleanArray
1886 arr = (
1887 element._values
1888 if isinstance(element, (ABCIndex, ABCSeries))
1889 else element
1890 )
1891 if arr._hasna: # type: ignore[union-attr]
1892 # i.e. there are pd.NA elements
1893 raise LossySetitemError
1894 return element
1895 # GH 57338 check boolean array set as object type
1896 if tipo.kind == "O" and isinstance(element, np.ndarray):
1897 if lib.is_bool_array(element):
1898 return element.astype("bool")
1899 raise LossySetitemError
1900 if lib.is_bool(element):
1901 return element
1902 raise LossySetitemError
1903
1904 if dtype.kind == "S":
1905 # TODO: test tests.frame.methods.test_replace tests get here,
1906 # need more targeted tests. xref phofl has a PR about this
1907 if tipo is not None:
1908 if tipo.kind == "S" and tipo.itemsize <= dtype.itemsize: # type: ignore[union-attr]
1909 return element
1910 raise LossySetitemError
1911 if isinstance(element, bytes) and len(element) <= dtype.itemsize:
1912 return element
1913 raise LossySetitemError
1914
1915 if dtype.kind == "V":
1916 # i.e. np.void, which cannot hold _anything_
1917 raise LossySetitemError
1918
1919 raise NotImplementedError(dtype)
1920
1921
1922def _dtype_can_hold_range(rng: range, dtype: np.dtype) -> bool:
1923 """
1924 _maybe_infer_dtype_type infers to int64 (and float64 for very large endpoints),
1925 but in many cases a range can be held by a smaller integer dtype.
1926 Check if this is one of those cases.
1927 """
1928 if not len(rng):
1929 return True
1930 return np_can_cast_scalar(rng.start, dtype) and np_can_cast_scalar(rng.stop, dtype)
1931
1932
1933def np_can_cast_scalar(element: Scalar, dtype: np.dtype) -> bool:
1934 """
1935 np.can_cast pandas-equivalent for pre 2-0 behavior that allowed scalar
1936 inference
1937
1938 Parameters
1939 ----------
1940 element : Scalar
1941 dtype : np.dtype
1942
1943 Returns
1944 -------
1945 bool
1946 """
1947 try:
1948 np_can_hold_element(dtype, element)
1949 return True
1950 except (LossySetitemError, NotImplementedError):
1951 return False