WEB: add pandas 3.0 announcement blog post

Adding a new blog post to point to in communication, and updating the announcement banner to point to that

(for the final release, a separate blog post is maybe less crucial compared to just pointing to the release notes directly. I could also add some highlights to that page)

@@ -1 +1 @@
-Announcement: pandas 3.0.0 release candidate ready for testing! Read more in the blog post
\ No newline at end of file
+Announcement: pandas 3.0 released! Read more in the blog post
\ No newline at end of file

@@ -0,0 +1,126 @@
+Title: pandas 3.0 released!
+Date: 2026-01-21
+
+# pandas 3.0 released!
+
+We're excited to announce the release of pandas 3.0.0. This major
+long-awaited release brings significant improvements to pandas, but also
+features some potentially breaking changes.
+
+## Highlights of pandas 3.0
+
+pandas 3.0 introduces several major enhancements:
+
+- **Dedicated string data type by default**: string columns are now inferred as
+  the new `str` dtype instead of `object`, providing better performance and type
+  safety
+- **Consistent copy/view behaviour with Copy-on-Write (CoW)** (a.k.a. getting
+  rid of the `SettingWithCopyWarning`): more predictable and consistent behavior
+  for all operations, with improved performance through avoiding unnecessary
+  copies
+- **New default resolution for datetime-like data**: no longer defaulting to
+  nanoseconds, but generally microseconds (or the resolution of the input), when
+  constructing datetime or timedelta data (avoiding out-of-bounds errors
+  for dates with a year before 1678 or after 2262)
+- **New `pd.col` syntax**: initial support for `pd.col()` as a simplified syntax
+  for creating callables in `DataFrame.assign`
+
+Further, pandas 3.0 includes a lot of other improvements and bug fixes. You can
+find the complete list of changes in the
+[release notes](https://pandas.pydata.org/docs/dev/whatsnew/v3.0.0.html).
+
+## Upgrading to pandas 3.0
+
+The pandas 3.0 release removed functionality that was deprecated in previous releases
+(see [here](https://pandas.pydata.org/docs/whatsnew/v3.0.0.html#whatsnew-300-prior-deprecations)
+for an overview). It is recommended to first upgrade to pandas 2.3 and to ensure
+your code is working without warnings, before upgrading to pandas 3.0.
+
+Further, as a major release, pandas 3.0 includes some breaking changes that may
+require updates to your code. The two most significant changes are the new
+string dtype and the copy/view behaviour changes, detailed below. An overview of
+all potentially breaking changes can be found in the [Backwards incompatible API
+changes](https://pandas.pydata.org/docs/whatsnew/v3.0.0.html#backwards-incompatible-api-changes)
+section of the release notes.
+
+### 1. Dedicated string data type by default
+
+Starting with pandas 3.0, string columns are automatically inferred as `str`
+dtype instead of the numpy `object` (which can store any Python object).
+
+**Example:**
+```python
+# Old behavior (pandas < 3.0)
+>>> ser = pd.Series(["a", "b"])
+>>> ser
+0    a
+1    b
+dtype: object  # <-- numpy object dtype
+
+# New behavior (pandas 3.0)
+>>> ser = pd.Series(["a", "b"])
+>>> ser.dtype
+>>> ser
+0    a
+1    b
+dtype: str  # <-- new string dtype
+```
+
+This change improves performance and type safety, but may require code updates,
+especially for library code that currently looks for "object" dtype when
+expecting string data.
+
+For more details, see the
+[migration guide for the new string data type](https://pandas.pydata.org/docs/dev/user_guide/migration-3-strings.html).
+
+This new data type will use the `pyarrow` library under the hood, if installed,
+to provide the performance improvements. Therefore we strongly recommend to
+install `pyarrow` alongside pandas (but `pyarrow` is not a required dependency
+installed by default).
+
+### 2. Consistent copy/view behaviour with Copy-on-Write (CoW)
+
+Copy-on-Write is now the default and only mode in pandas 3.0. This makes
+behavior more consistent and predictable, and avoids a lot of defensive copying
+(improving performance), but requires updates to certain coding patterns.
+
+The most impactfull change is that **chained assignment will no longer work**.
+As a result, the `SettingWithCopyWarning` is also removed (since there is no
+longer ambiguity whether it would work or not), and defensive `.copy()` calls
+to silence the warning are no longer needed.
+
+**Example:**
+```python
+# Old behavior (pandas < 3.0) - chained assignment
+df["foo"][df["bar"] > 5] =   # This might modify df (unpredictable)
+
+# New behavior (pandas 3.0) - must do the modification in one step (e.g. with .loc)
+df.loc[df["bar"] > 5, "foo"] = 100
+```
+
+In general, any result of an indexing operation or method now always behaves as
+if it were a copy, so modifications of the result won't affect the original
+DataFrame.
+
+For more details, see the
+[Copy-on-Write migration guide](https://pandas.pydata.org/docs/dev/user_guide/copy_on_write.html#migrating-to-copy-on-write).
+
+## Obtaining pandas 3.0
+
+You can install the latest pandas 3.0 release from PyPI:
+
+```bash
+python -m pip install --upgrade pandas==3.0.*
+```
+
+Or from conda-forge using conda/mamba:
+
+```bash
+conda install -c conda-forge pandas=3.0
+```
+
+## Running into an issue or regression?
+
+Please report any problem you encounter with the release on the pandas [issue tracker](https://github.com/[USER_REDACTED]ndas-dev/pandas/issues).
+
+Thanks to [NAME_REDACTED] made this release possible!

found a few things

I'm good with it.

The initial `(` ends on the next line.

```suggestion
(see [here](https://pandas.pydata.org/docs/whatsnew/v3.0.0.html#whatsnew-300-prior-deprecations)
```

```suggestion
and the copy/view behaviour changes, detailed below. An overview of all potentially
```

I would also mention performance here.

```suggestion
  rid of the `SettingWithCopyWarning`): more predictable and consistent behavior
```

```suggestion
  constructing datetime or timedelta data (avoiding out-of-bounds errors
```

```suggestion
### Obtaining pandas 3.0
```

Do we want to say anything in this post about how things are affected by [NAME_REDACTED] `pyarrow` is installed, and that it is NOT required to install `pyarrow` ?

I added a note about that in the section about the string dtype below


BUG: Fix data races in block internals

This PR fixes several thread safety issues in Pandas internals on the free-threaded build. We found all of them using thread sanitizer testing.

I also added a multithreaded test based on the original test in statsmodels we used to find these issues. There's probably lots more room to add more multithreaded tests, but a real-world example that triggered real issues seems as good a place to start as any to me.

You can read more about critical sections in [the CPython docs](https://docs.python.org/3/howto/free-threading-extensions.html#critical-sections), the [cython docs](https://cython.readthedocs.io/en/latest/src/userguide/freethreading.html#critical-sections), and the [free-threading guide](https://py-free-threading.github.io/porting-extensions/#critical-sections).

- [x] closes #63685
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.
- [ ] If I used AI to develop this pull request, I prompted it to follow `AGENTS.md`.

@@ -27,6 +27,7 @@ Bug fixes
 - Fixed a bug in :func:`col` where unary operators (``-``, ``+``, ``abs``) were not supported (:issue:`63939`)
 - Fixed a bug in the :func:`comparison_op` raising a ``TypeError`` for zerodim
   subclasses of ``np.ndarray`` (:issue:`63205`)
+- Fixed thread safety issues in :class:`DataFrame` internals on the free-threaded build (:issue:`63685`).

 .. ---------------------------------------------------------------------------
 .. _whatsnew_301.contributors:

@@ -8,7 +8,7 @@ dependencies:

   # build dependencies
   - versioneer
-  - cython<4.0.0a0
+  - cython>=3.1.0,<4.0.0a0
   - meson>=1.2.1,<2
   - meson-python>=0.17.1,<1

@@ -272,6 +272,7 @@ cdef class IndexEngine:
                 self.monotonic_dec = 1

     [USER_REDACTED]
+    [USER_REDACTED].critical_section
     def is_unique(self) -> bool:
         # for why we check is_monotonic_increasing here, see
         # https://github.com/[USER_REDACTED]ndas-dev/pandas/pull/55342#discussion_r1361405781
@@ -286,13 +287,15 @@ cdef class IndexEngine:
         self._ensure_mapping_populated()

     [USER_REDACTED]
+    [USER_REDACTED].critical_section
     def is_monotonic_increasing(self) -> bool:
         if self.need_monotonic_check:
             self._do_monotonic_check()

         return self.monotonic_inc == 1

     [USER_REDACTED]
+    [USER_REDACTED].critical_section
     def is_monotonic_decreasing(self) -> bool:
         if self.need_monotonic_check:
             self._do_monotonic_check()
@@ -336,24 +339,26 @@ cdef class IndexEngine:
         return val

     [USER_REDACTED]
+    [USER_REDACTED].critical_section
     def is_mapping_populated(self) -> bool:
         return self.mapping is not None

+    [USER_REDACTED].critical_section
     cdef _ensure_mapping_populated(self):
         # this populates the mapping
         # if its not already populated
         # also satisfies the need_unique_check
-
         if not self.is_mapping_populated:
-
             values = self.values
-            self.mapping = self._make_hash_table(len(values))
-            self.mapping.map_locations(values, self.mask)
-
-            if len(self.mapping) == len(values):
-                self.unique = 1
-
-        self.need_unique_check = 0
+            mapping = self._make_hash_table(len(values))
+            mapping.map_locations(values, self.mask)
+            unique = len(mapping) == len(values)
+        # check again after creating the mapping
+        # which could have released the critical_section
+        if not self.is_mapping_populated:
+            self.mapping = mapping
+            self.unique = unique
+            self.need_unique_check = 0

     def clear_mapping(self):
         self.mapping = None

@@ -139,6 +139,7 @@ cdef class BlockPlacement:
             return self._as_array

     [USER_REDACTED]
+    [USER_REDACTED].critical_section
     def as_array(self) -> np.ndarray:
         cdef:
             Py_ssize_t start, stop, _
@@ -147,8 +148,10 @@ cdef class BlockPlacement:
             start, stop, step, _ = slice_get_indices_ex(self._as_slice)
             # NOTE: this is the C-optimized equivalent of
             #  `np.arange(start, stop, step, dtype=np.intp)`
-            self._as_array = cnp.PyArray_Arange(start, stop, step, NPY_INTP)
-            self._has_array = True
+            as_array = cnp.PyArray_Arange(start, stop, step, NPY_INTP)
+            if not self._has_array:
+                self._as_array = as_array
+                self._has_array = True

         return self._as_array

@@ -221,11 +224,14 @@ cdef class BlockPlacement:
         # We can get here with int or ndarray
         return self.iadd(other)

+    [USER_REDACTED].critical_section
     cdef slice _ensure_has_slice(self):
         if not self._has_slice:
-            self._as_slice = indexer_as_slice(self._as_array)
-            self._has_slice = True
-
+            as_slice = indexer_as_slice(self._as_array)
+            # check again after indexer_as_slice call
+            if not self._has_slice:
+                self._as_slice = as_slice
+                self._has_slice = True
         return self._as_slice

     cpdef BlockPlacement increment_above(self, Py_ssize_t loc):
@@ -730,7 +736,7 @@ cdef class BlockManager:
         public tuple blocks
         public list axes
         public bint _known_consolidated, _is_consolidated
-        public ndarray _blknos, _blklocs
+        ndarray __blknos, __blklocs

     def __cinit__(
         self,
@@ -758,7 +764,27 @@ cdef class BlockManager:

     # -------------------------------------------------------------------
     # [NAME_REDACTED]
+    [USER_REDACTED]
+    [USER_REDACTED].critical_section
+    def _blknos(self):
+        return self.__blknos
+
+    @_blknos.setter
+    [USER_REDACTED].critical_section
+    def _blknos(self, ndarray val):
+        self.__blknos = val
+
+    [USER_REDACTED]
+    [USER_REDACTED].critical_section
+    def _blklocs(self):
+        return self.__blklocs
+
+    @_blklocs.setter
+    [USER_REDACTED].critical_section
+    def _blklocs(self, ndarray val):
+        self.__blklocs = val

+    [USER_REDACTED].critical_section
     cpdef _rebuild_blknos_and_blklocs(self):
         """
         Update mgr._blknos / mgr._blklocs.
@@ -796,8 +822,8 @@ cdef class BlockManager:
             if blkno == -1:
                 raise AssertionError("Gaps in blk ref_locs")

-        self._blknos = new_blknos
-        self._blklocs = new_blklocs
+        self.__blknos = new_blknos
+        self.__blklocs = new_blklocs

     # -------------------------------------------------------------------
     # Pickle
@@ -869,8 +895,9 @@ cdef class BlockManager:
         mgr = type(self)(tuple(nbs), new_axes, verify_integrity=False)

         # We can avoid having to rebuild blklocs/blknos
-        blklocs = self._blklocs
-        blknos = self._blknos
+        with cython.critical_section(self):
+            blklocs = self.__blklocs
+            blknos = self.__blknos
         if blknos is not None:
             mgr._blknos = blknos.copy()
             mgr._blklocs = blklocs.copy()

@@ -1518,6 +1518,7 @@ cdef class _Timedelta(timedelta):
         self._ensure_components()
         return -999999999 <= self._d and self._d <= 999999999

+    [USER_REDACTED].critical_section
     cdef _ensure_components(_Timedelta self):
         """
         compute the components
@@ -1529,6 +1530,8 @@ cdef class _Timedelta(timedelta):
             pandas_timedeltastruct tds

         pandas_timedelta_to_timedeltastruct(self._value, self._creso, &tds)
+        if self._is_populated:
+            return
         self._d = tds.days
         self._h = tds.hrs
         self._m = tds.min

@@ -1,9 +1,11 @@
 from __future__ import annotations

+from concurrent.futures import ThreadPoolExecutor
 from decimal import Decimal
 import operator
 import os
 from sys import byteorder
+import threading
 from typing import (
     TYPE_CHECKING,
     ContextManager,
@@ -535,6 +537,36 @@ def shares_memory(left, right) -> bool:
     raise NotImplementedError(type(left), type(right))

+def run_multithreaded(closure, max_workers, arguments=None, pass_barrier=False):
+    with ThreadPoolExecutor(max_workers=max_workers) as tpe:
+        if arguments is None:
+            arguments = []
+        else:
+            arguments = list(arguments)
+
+        if pass_barrier:
+            barrier = threading.Barrier(max_workers)
+            arguments.append(barrier)
+
+        try:
+            futures = []
+            for _ in range(max_workers):
+                futures.append(tpe.submit(closure, *arguments))  # noqa: PERF401
+        except RuntimeError as e:
+            import pytest
+
+            pytest.skip(
+                f"Spawning {max_workers} threads failed with "
+                f"error {e!r} (likely due to resource limits on the "
+                "system running the tests)"
+            )
+        finally:
+            if len(futures) < max_workers and pass_barrier:
+                barrier.abort()
+        for f in futures:
+            f.result()
+
+
 __all__ = [
     "ALL_INT_EA_DTYPES",
     "ALL_INT_NUMPY_DTYPES",
@@ -602,6 +634,7 @@ def shares_memory(left, right) -> bool:
     "raises_chained_assignment_error",
     "round_trip_pathlib",
     "round_trip_pickle",
+    "run_multithreaded",
     "set_locale",
     "set_timezone",
     "setitem",

@@ -2,12 +2,15 @@
     datetime,
     timedelta,
 )
+from io import StringIO
 import itertools
+from textwrap import dedent

 import numpy as np
 import pytest

 from pandas.errors import Pandas4Warning
+import pandas.util._test_decorators as td

 import pandas as pd
 from pandas import (
@@ -383,3 +386,70 @@ def test_update_inplace_sets_valid_block_values():

     # check we haven't put a Series into any block.values
     assert isinstance(df._mgr.blocks[0].values, Categorical)
+
+
+def get_longley_data():
+    # From statsmodels.datasets.longley
+    # This specific dataset seems to trigger races in Pandas 3.0.0 more readily
+    # than data frames used elsewhere in the tests
+    longley_csv = StringIO(
+        dedent(
+            """"Obs","GNPDEFL","GNP","UNEMP","ARMED","POP","YEAR"
+            1,83,234289,2356,1590,107608,1947
+            2,88.5,259426,2325,1456,108632,1948
+            3,88.2,258054,3682,1616,109773,1949
+            4,89.5,284599,3351,1650,110929,1950
+            5,96.2,328975,2099,3099,112075,1951
+            6,98.1,346999,1932,3594,113270,1952
+            7,99,365385,1870,3547,115094,1953
+            8,100,363112,3578,3350,116219,1954
+            9,101.2,397469,2904,3048,117388,1955
+            10,104.6,419180,2822,2857,118734,1956
+            11,108.4,442769,2936,2798,120445,1957
+            12,110.8,444546,4681,2637,121950,1958
+            13,112.6,482704,3813,2552,123366,1959
+            14,114.2,502601,3931,2514,125368,1960
+            15,115.7,518173,4806,2572,127852,1961
+            16,116.9,554894,4007,2827,130081,1962
+            """
+        )
+    )
+
+    return pd.read_csv(longley_csv).iloc[:, [1, 2, 3, 4, 5, 6]].astype(float)
+
+
+# See gh-63685, comparisons and copying led to races in statsmodels tests
+#
+# This test spawns a thread pool, so it shouldn't run under xdist.
+# It generates warnings, so it needs warnings to be thread-safe as well
+[USER_REDACTED].skip_if_thread_unsafe_warnings
+[USER_REDACTED].mark.single_cpu
+def test_multithreaded_reading():
+    def numpy_assert(data, b):
+        b.wait()
+        tm.assert_almost_equal((data + 1) - 1, data.copy())
+
+    tm.run_multithreaded(
+        numpy_assert, max_workers=8, arguments=(get_longley_data(),), pass_barrier=True
+    )
+
+    def safe_is_const(s):
+        try:
+            return np.ptp(s) == 0.0 and np.any(s != 0.0)
+        except Exception:
+            return False
+
+    def concat(data, b):
+        b.wait()
+        x = data.copy()
+        nobs = len(x)
+        trendarr = np.fliplr(np.vander(np.arange(1, nobs + 1, dtype=np.float64), 1))
+        x.apply(safe_is_const, 0)
+        trendarr = DataFrame(trendarr, index=x.index, columns=["const"])
+        x = [trendarr, x]
+        x = pd.concat(x[::1], axis=1)
+        tm.assert_frame_equal(x, x)
+
+    tm.run_multithreaded(
+        concat, max_workers=8, arguments=(get_longley_data(),), pass_barrier=True
+    )

@@ -27,6 +27,7 @@ def test_foo():
 from __future__ import annotations

 import locale
+import sys
 from typing import TYPE_CHECKING

 import pytest
@@ -118,6 +119,10 @@ def skip_if_no(package: str, min_version: str | None = None) -> pytest.MarkDecor
     WASM,
     reason="does not support wasm",
 )
+skip_if_thread_unsafe_warnings = pytest.mark.skipif(
+    not getattr(sys.flags, "context_aware_warnings", 0),
+    reason="Python warnings must be thread-safe for consistent results",
+)

 def parametrize_fixture_doc(*args) -> Callable[[F], F]:

@@ -5,7 +5,7 @@ requires = [
     "meson-python>=0.17.1,<1",
     "meson>=1.2.1,<2",
     "wheel",
-    "Cython<4.0.0a0",  # Note: sync with setup.py, environment.yml and asv.conf.json
+    "Cython>3.1.0,<4.0.0a0",  # Note: sync with setup.py, environment.yml and asv.conf.json
     # Force numpy higher than 2.0, so that built wheels are compatible
     # with both numpy 1 and 2
     "numpy>=2.0.0",

@@ -37,7 +37,7 @@ def is_platform_mac():

 # note: sync with pyproject.toml, environment.yml and asv.conf.json
-min_cython_ver = "3.0"
+min_cython_ver = "3.1"

 try:
     from Cython import (

Do you think this tests is reliable (non-flaky) to run with pytest-xdist on the GHA runners? Decorating this tests with `[USER_REDACTED].mark.single_cpu` will run this test without xdist

Good call, I added the mark and also added some explanatory comments.

```suggestion
skip_if_thread_unsafe_warnings = pytest.mark.skipif(
```
naming nit


DOC: update whatsnew section on datetimelike resolution

Rewriting this section now all the different changes have landed, in an attempt to make it a bit more coherent (and some things were also a bit outdated after all the changes).

@@ -398,8 +398,8 @@ or resolution of the datetime64 dtype:
 .. ipython:: python

    stamps.astype(np.int64)
-   stamps.astype("datetime64[s]").astype(np.int64)
-   stamps.astype("datetime64[ms]").astype(np.int64)
+   stamps.as_unit("s").astype(np.int64)
+   stamps.as_unit("ns").astype(np.int64)

 .. _timeseries.origin:

@@ -343,74 +343,68 @@ Backwards incompatible API changes

 .. _whatsnew_300.api_breaking.datetime_resolution_inference:

-Datetime resolution inference
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Datetime/timedelta resolution inference
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

-Converting a sequence of strings, ``datetime`` objects, or ``np.datetime64`` objects to
-a ``datetime64`` dtype now performs inference on the appropriate resolution (AKA unit) for the output dtype. This affects :class:`Series`, :class:`DataFrame`, :class:`Index`, :class:`DatetimeIndex`, and :func:`to_datetime`.
+Prior to pandas 3.0, whenever converting a sequence of of strings, stdlib ``datetime``
+objects, ``np.datetime64`` objects, or integers to a ``datetime64`` / ``timedelta64``
+dtype, this would always have resulted in nanosecond resolution (or raised an
+out-of-bounds error).
+Now it performs inference on the appropriate resolution (a.k.a. ``unit``) for the output dtype. This affects both the generric constructors (:class:`Series`, :class:`DataFrame`, :class:`Index`, :class:`DatetimeIndex`) as specific conversion or creation functions (:func:`to_datetime`, :func:`to_timedelta`, :func:`date_range`, :func:`timedelta_range`, :class:`Timestamp`, :class:`Timedelta`).

-Previously, these would always give nanosecond resolution:
+The general rules for the various types of input:

-.. code-block:: ipython
-
-    In [1]: dt = pd.Timestamp("2024-03-22 11:36").to_pydatetime()
-    In [2]: pd.to_datetime([dt]).dtype
-    Out[2]: dtype('<M8[ns]')
-    In [3]: pd.Index([dt]).dtype
-    Out[3]: dtype('<M8[ns]')
-    In [4]: pd.DatetimeIndex([dt]).dtype
-    Out[4]: dtype('<M8[ns]')
-    In [5]: pd.Series([dt]).dtype
-    Out[5]: dtype('<M8[ns]')
+- The new default resolution when parsing strings is microseconds, falling back
+  to nanoseconds when the precision of the string requires it.
+- The resolution of the input is preserved for stdlib  ``datetime`` objects (i.e. microseconds)
+  or ``np.datetime64``/``np.timedelta64`` objects (i.e. the unit, capped to
+  the supported range of seconds to nanoseconds).
+- For integer input, the resolution how the integer value is interpreted is used
+  as the resulting resolution (or capped to the supported range of seconds to nanoseconds).

-This now infers the unit microsecond unit "us" from the pydatetime object, matching the scalar :class:`Timestamp` behavior.
+For example, the following would always have given nanosecond resolution previously,
+but now infer:

 .. ipython:: python

-    In [1]: dt = pd.Timestamp("2024-03-22 11:36").to_pydatetime()
-    In [2]: pd.to_datetime([dt]).dtype
-    In [3]: pd.Index([dt]).dtype
-    In [4]: pd.DatetimeIndex([dt]).dtype
-    In [5]: pd.Series([dt]).dtype
+    # parsing strings
+    print(pd.to_datetime(["2024-03-22 11:36"]).dtype)
+    # converting integers
+    print(pd.to_datetime([0], unit="s").dtype)
+    # converting stdlib datetime object
+    dt = pd.Timestamp("2024-03-22 11:36").to_pydatetime()
+    print(pd.to_datetime([dt]).dtype)
+    # the same when inferring a datetime dtype in the generic constructors
+    print(pd.Series([dt]).dtype)
+    # converting numpy objects
+    print(pd.Series([np.datetime64("2024-03-22", "ms")]).dtype)

 Similar when passed a sequence of ``np.datetime64`` objects, the resolution of the passed objects will be retained (or for lower-than-second resolution, second resolution will be used).

-When passing strings, the resolution will depend on the precision of the string, again matching the :class:`Timestamp` behavior. Previously:
-
-.. code-block:: ipython
-
-    In [2]: pd.to_datetime(["2024-03-22 11:43:01"]).dtype
-    Out[2]: dtype('<M8[ns]')
-    In [3]: pd.to_datetime(["2024-03-22 11:43:01.002"]).dtype
-    Out[3]: dtype('<M8[ns]')
-    In [4]: pd.to_datetime(["2024-03-22 11:43:01.002003"]).dtype
-    Out[4]: dtype('<M8[ns]')
-    In [5]: pd.to_datetime(["2024-03-22 11:43:01.002003004"]).dtype
-    Out[5]: dtype('<M8[ns]')
-
-The inferred resolution now matches that of the input strings for nanosecond-precision strings, otherwise defaulting to microseconds:
+When parsing strings, the default is now microseconds (which also impacts I/O
+methods reading from text files, such as :func:`read_csv` and :func:`read_json`).
+Except when the string has nanosecond precision, in which case nanosecond resolution is used:

 .. ipython:: python

-    In [2]: pd.to_datetime(["2024-03-22 11:43:01"]).dtype
-    In [3]: pd.to_datetime(["2024-03-22 11:43:01.002"]).dtype
-    In [4]: pd.to_datetime(["2024-03-22 11:43:01.002003"]).dtype
-    In [5]: pd.to_datetime(["2024-03-22 11:43:01.002003004"]).dtype
-
-This is also a change for the :class:`Timestamp` constructor with a string input, which in version 2.x.y could give second or millisecond unit, which users generally disliked (:issue:`52653`)
+    print(pd.to_datetime(["2024-03-22 11:43:01.123"]).dtype)
+    print(pd.to_datetime(["2024-03-22 11:43:01.123456"]).dtype)
+    print(pd.to_datetime(["2024-03-22 11:43:01.123456789"]).dtype)

-In cases with mixed-resolution inputs, the highest resolution is used:
-
-.. code-block:: ipython
+This is also a change for the :class:`Timestamp` constructor with a string input, which in version 2.x.y could give second or millisecond unit (:issue:`52653`).

-    In [2]: pd.to_datetime([pd.Timestamp("2024-03-22 11:43:01"), "2024-03-22 11:43:01.002"]).dtype
-    Out[2]: dtype('<M8[ns]')
+.. warning::

-.. warning:: Many users will now get "M8[us]" dtype data in cases when they used to get "M8[ns]". For most use cases they should not notice a difference. One big exception is converting to integers, which will give integers 1000x smaller.
+    Many users will now get "datetime64[us]" dtype data in cases when they used
+    to get "datetime64[ns]".
+    For most use cases they should not notice a difference. One big exception is
+    converting to integers, which will give integers 1000x smaller.

-Similarly, the :class:`Timedelta` constructor and :func:`to_timedelta` with a string input now defaults to a microsecond unit, using nanosecond unit only in cases that actually have nanosecond precision.
+    When converting datetime-like data to integers, it is recommended to avoid
+    ``astype("int64")`` to make your code agnostic to the exact unit, or ensure
+    the desired unit with :func:`~Series.dt.as_unit` before casting.

-Moreover, passing an integer to the :class:`Timedelta` constructor or :func:`to_timedelta` along with a ``unit`` will now return an object with that unit when possible, or the closest-supported unit for non-supported units ("W", "D", "h", "m").
+    See :ref:`timeseries.converting.epoch_inverse` for more details.

 .. _whatsnew_300.api_breaking.concat_datetime_sorting:

"as specific" -> "and specific"?

other than this, looks good.