Repository: OceanParcels/Parcels
Pull request #1743 (2024-10-25 09:22:40)

New and updated `reprs` for Variable, ParticleFile, Field, VectorField, and ParticleSet
New/updated reprs for:

- Variable
- ParticleFile
- Field
- VectorField
- ParticleSet
- FieldSet (partial)

Unfortunately with FieldSet the field name (i.e., U, V, UV) is only available as the `attr` name. Surfacing field names in the repr is dependent on #1663 .

Samples:

```
Variable(name=test, dtype=<class 'numpy.float32'>, initial=0, to_write=False)

ParticleFile(name='file.zarr', particleset=<parcels.particleset.ParticleSet object at 0x15e6896a0>, outputdt=3600.0, chunks=None, create_new_zarrfile=False)

<Field>
    grid            : RectilinearZGrid(lon=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), lat=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), time=array([ 0.00]), time_origin=0, mesh='flat')
    extrapolate time: True
    time_periodic   : False
    gridindexingtype: 'nemo'
    to_write        : False

<ParticleSet>
    fieldset   : <parcels.fieldset.FieldSet object at 0x15faeb3b0>
    pclass     : False
    repeatdt   : None
    # particles: 15
    particles  : [P[2](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[3](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[4](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[5](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[6](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[7](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), P[8](lon=0.000000, lat=0.000000, depth=0.000000, time=0.000000), ...]


<FieldSet>
    fields:
        <Field>
            grid            : RectilinearZGrid(lon=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), lat=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), time=array([ 0.00]), time_origin=0, mesh='flat')
            extrapolate time: True
            time_periodic   : False
            gridindexingtype: 'nemo'
            to_write        : False
        <Field>
            grid            : RectilinearZGrid(lon=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), lat=array([ 0.00,  0.05,  0.11, ...,  0.89,  0.95,  1.00], dtype=float32), time=array([ 0.00]), time_origin=0, mesh='flat')
            extrapolate time: True
            time_periodic   : False
            gridindexingtype: 'nemo'
            to_write        : False
        <parcels.field.VectorField object at 0x16bed5070>
```



contributes to #1693


Patch:
diff --git a/docs/examples/parcels_tutorial.ipynb b/docs/examples/parcels_tutorial.ipynb
index 5936644a..102a8741 100644
--- a/docs/examples/parcels_tutorial.ipynb
+++ b/docs/examples/parcels_tutorial.ipynb
@@ -72,11 +72,41 @@
    "cell_type": "code",
    "execution_count": 2,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "<FieldSet>\n",
+      "    fields:\n",
+      "        <Field>\n",
+      "            name            : 'U'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00,  172800.00, ...,  432000.00,  518400.00,  604800.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <Field>\n",
+      "            name            : 'V'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00,  172800.00, ...,  432000.00,  518400.00,  604800.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <VectorField>\n",
+      "            name: 'UV'\n",
+      "            U: <parcels.field.Field object at 0x1580e0980>\n",
+      "            V: <parcels.field.Field object at 0x1585d8920>\n",
+      "            W: None\n"
+     ]
+    }
+   ],
    "source": [
     "example_dataset_folder = parcels.download_example_dataset(\"MovingEddies_data\")\n",
     "\n",
-    "fieldset = parcels.FieldSet.from_parcels(f\"{example_dataset_folder}/moving_eddies\")"
+    "fieldset = parcels.FieldSet.from_parcels(f\"{example_dataset_folder}/moving_eddies\")\n",
+    "\n",
+    "print(fieldset)"
    ]
   },
   {
@@ -152,8 +182,35 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "P[0](lon=330000.000000, lat=100000.000000, depth=0.000000, time=not_yet_set)\n",
-      "P[1](lon=330000.000000, lat=280000.000000, depth=0.000000, time=not_yet_set)\n"
+      "<ParticleSet>\n",
+      "    fieldset   : <FieldSet>\n",
+      "    fields:\n",
+      "        <Field>\n",
+      "            name            : 'U'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <Field>\n",
+      "            name            : 'V'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <VectorField>\n",
+      "            name: 'UV'\n",
+      "            U: <parcels.field.Field object at 0x1580e0980>\n",
+      "            V: <parcels.field.Field object at 0x1585d8920>\n",
+      "            W: None\n",
+      "    pclass     : <class 'parcels.particle.JITParticle'>\n",
+      "    repeatdt   : None\n",
+      "    # particles: 2\n",
+      "    particles  : [\n",
+      "        P[0](lon=330000.000000, lat=100000.000000, depth=0.000000, time=not_yet_set),\n",
+      "        P[1](lon=330000.000000, lat=280000.000000, depth=0.000000, time=not_yet_set)\n",
+      "    ]\n"
      ]
     }
    ],
@@ -208,7 +265,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The final step is to run (or 'execute') the `ParticelSet`. We run the particles using the `AdvectionRK4` kernel, which is a 4th order Runge-Kutte implementation that comes with Parcels. We run the particles for 6 days (using the `timedelta` function from `datetime`), at an RK4 timestep of 5 minutes. We store the trajectory information at an interval of 1 hour in a file called `EddyParticles.zarr`. Because `time` was `not_yet_set`, the particles will be advected from the first date available in the `fieldset`, which is the default behaviour.\n"
+    "We can now create `ParticleFile` objects to store the output of the particles. We will store the output at an interval of 1 hour in a `zarr` file called `EddyParticles.zar`. See the [Working with Parcels output tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_output.html) for more information on the `zarr` format."
    ]
   },
   {
@@ -220,8 +277,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "INFO: Output files are stored in EddyParticles.zarr.\n",
-      "100%|██████████| 518400.0/518400.0 [00:03<00:00, 172443.78it/s]\n"
+      "ParticleFile(name='EddyParticles.zarr', particleset=<parcels.particleset.ParticleSet object at 0x1584ee7e0>, outputdt=3600.0, chunks=None, create_new_zarrfile=True)\n"
      ]
     }
    ],
@@ -230,6 +286,32 @@
     "    name=\"EddyParticles.zarr\",  # the file name\n",
     "    outputdt=timedelta(hours=1),  # the time step of the outputs\n",
     ")\n",
+    "\n",
+    "print(output_file)"
+   ]
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The final step is to run (or 'execute') the `ParticelSet`. We run the particles using the `AdvectionRK4` kernel, which is a 4th order Runge-Kutte implementation that comes with Parcels. We run the particles for 6 days (using the `timedelta` function from `datetime`), at an RK4 timestep of 5 minutes. Because `time` was `not_yet_set`, the particles will be advected from the first date available in the `fieldset`, which is the default behaviour.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "100%|██████████| 518400.0/518400.0 [00:05<00:00, 95421.55it/s] \n"
+     ]
+    }
+   ],
+   "source": [
     "pset.execute(\n",
     "    parcels.AdvectionRK4,  # the kernel (which defines how particles move)\n",
     "    runtime=timedelta(days=6),  # the total length of the run\n",
@@ -248,15 +330,42 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "P[0](lon=226905.562500, lat=82515.218750, depth=0.000000, time=518100.000000)\n",
-      "P[1](lon=260835.125000, lat=320403.343750, depth=0.000000, time=518100.000000)\n"
+      "<ParticleSet>\n",
+      "    fieldset   : <FieldSet>\n",
+      "    fields:\n",
+      "        <Field>\n",
+      "            name            : 'U'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 432000.00,  518400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <Field>\n",
+      "            name            : 'V'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 432000.00,  518400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <VectorField>\n",
+      "            name: 'UV'\n",
+      "            U: <parcels.field.Field object at 0x1580e0980>\n",
+      "            V: <parcels.field.Field object at 0x1585d8920>\n",
+      "            W: None\n",
+      "    pclass     : <class 'parcels.particle.JITParticle'>\n",
+      "    repeatdt   : None\n",
+      "    # particles: 2\n",
+      "    particles  : [\n",
+      "        P[0](lon=226905.562500, lat=82515.218750, depth=0.000000, time=518100.000000),\n",
+      "        P[1](lon=260835.125000, lat=320403.343750, depth=0.000000, time=518100.000000)\n",
+      "    ]\n"
      ]
     },
     {
@@ -300,7 +409,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [
     {
@@ -333,7 +442,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -371,7 +480,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 12,
    "metadata": {},
    "outputs": [
     {
@@ -13159,7 +13268,7 @@
        "<IPython.core.display.HTML object>"
       ]
      },
-     "execution_count": 11,
+     "execution_count": 12,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -13186,7 +13295,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [
     {
@@ -13221,15 +13330,42 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "P[0](lon=329983.281250, lat=100495.609375, depth=0.000000, time=300.000000)\n",
-      "P[1](lon=330289.968750, lat=280418.906250, depth=0.000000, time=300.000000)\n"
+      "<ParticleSet>\n",
+      "    fieldset   : <FieldSet>\n",
+      "    fields:\n",
+      "        <Field>\n",
+      "            name            : 'U'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <Field>\n",
+      "            name            : 'V'\n",
+      "            grid            : RectilinearZGrid(lon=array([ 0.00,  2010.05,  4020.10, ...,  395979.91,  397989.94,  400000.00], dtype=float32), lat=array([ 0.00,  2005.73,  4011.46, ...,  695988.56,  697994.25,  700000.00], dtype=float32), time=array([ 0.00,  86400.00]), time_origin=0.0, mesh='flat')\n",
+      "            extrapolate time: False\n",
+      "            time_periodic   : False\n",
+      "            gridindexingtype: 'nemo'\n",
+      "            to_write        : False\n",
+      "        <VectorField>\n",
+      "            name: 'UV'\n",
+      "            U: <parcels.field.Field object at 0x1580e0980>\n",
+      "            V: <parcels.field.Field object at 0x1585d8920>\n",
+      "            W: None\n",
+      "    pclass     : <class 'parcels.particle.JITParticle'>\n",
+      "    repeatdt   : None\n",
+      "    # particles: 2\n",
+      "    particles  : [\n",
+      "        P[0](lon=329983.281250, lat=100495.609375, depth=0.000000, time=300.000000),\n",
+      "        P[1](lon=330289.968750, lat=280418.906250, depth=0.000000, time=300.000000)\n",
+      "    ]\n"
      ]
     },
     {
@@ -13281,7 +13417,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 15,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13308,7 +13444,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 16,
    "metadata": {},
    "outputs": [
     {
@@ -13346,7 +13482,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 17,
    "metadata": {},
    "outputs": [
     {
@@ -13405,7 +13541,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 18,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13427,7 +13563,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 19,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13448,7 +13584,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 20,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13465,7 +13601,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 21,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13488,7 +13624,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 22,
    "metadata": {},
    "outputs": [
     {
@@ -13522,7 +13658,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 23,
    "metadata": {},
    "outputs": [
     {
@@ -13568,7 +13704,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 24,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13590,7 +13726,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 25,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13620,7 +13756,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": 26,
    "metadata": {},
    "outputs": [
     {
@@ -13663,7 +13799,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": 27,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13715,7 +13851,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 28,
+   "execution_count": 29,
    "metadata": {},
    "outputs": [
     {
@@ -13775,7 +13911,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 29,
+   "execution_count": 30,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13802,7 +13938,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 30,
+   "execution_count": 31,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13841,7 +13977,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 31,
+   "execution_count": 32,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -13871,7 +14007,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 32,
+   "execution_count": 33,
    "metadata": {},
    "outputs": [
     {
@@ -13904,7 +14040,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 33,
+   "execution_count": 34,
    "metadata": {},
    "outputs": [
     {
diff --git a/parcels/field.py b/parcels/field.py
index c7f5d2ea..a8a79dfc 100644
--- a/parcels/field.py
+++ b/parcels/field.py
@@ -20,7 +20,7 @@ from parcels._typing import (
     assert_valid_gridindexingtype,
     assert_valid_interp_method,
 )
-from parcels.tools._helpers import deprecated_made_private, timedelta_to_float
+from parcels.tools._helpers import default_repr, deprecated_made_private, field_repr, timedelta_to_float
 from parcels.tools.converters import (
     Geographic,
     GeographicPolar,
@@ -149,6 +149,8 @@ class Field:
     * `Nested Fields <../examples/tutorial_NestedFields.ipynb>`__
     """
 
+    allow_time_extrapolation: bool
+    time_periodic: TimePeriodic
     _cast_data_dtype: type[np.float32] | type[np.float64]
 
     def __init__(
@@ -321,6 +323,9 @@ class Field:
         if len(kwargs) > 0:
             raise SyntaxError(f'Field received an unexpected keyword argument "{list(kwargs.keys())[0]}"')
 
+    def __repr__(self) -> str:
+        return field_repr(self)
+
     @property
     @deprecated_made_private  # TODO: Remove 6 months after v3.1.0
     def dataFiles(self):
@@ -1915,6 +1920,14 @@ class VectorField:
                 assert W.interp_method == "cgrid_velocity", "Interpolation methods of U and W are not the same."
                 assert self._check_grid_dimensions(U.grid, W.grid), "Dimensions of U and W are not the same."
 
+    def __repr__(self):
+        w_repr = default_repr(self.W) if self.W is not None else repr(self.W)
+        return f"""<{type(self).__name__}>
+    name: {self.name!r}
+    U: {default_repr(self.U)}
+    V: {default_repr(self.V)}
+    W: {w_repr}"""
+
     @staticmethod
     def _check_grid_dimensions(grid1, grid2):
         return (
diff --git a/parcels/fieldset.py b/parcels/fieldset.py
index aad0635f..9a7fd39d 100644
--- a/parcels/fieldset.py
+++ b/parcels/fieldset.py
@@ -14,7 +14,7 @@ from parcels.field import DeferredArray, Field, NestedField, VectorField
 from parcels.grid import Grid
 from parcels.gridset import GridSet
 from parcels.particlefile import ParticleFile
-from parcels.tools._helpers import deprecated_made_private
+from parcels.tools._helpers import deprecated_made_private, fieldset_repr
 from parcels.tools.converters import TimeConverter, convert_xarray_time_units
 from parcels.tools.loggers import logger
 from parcels.tools.statuscodes import TimeExtrapolationError
@@ -56,6 +56,9 @@ class FieldSet:
         self.compute_on_defer = None
         self._add_UVfield()
 
+    def __repr__(self):
+        return fieldset_repr(self)
+
     @property
     def particlefile(self):
         return self._particlefile
diff --git a/parcels/particle.py b/parcels/particle.py
index c7f1681e..87dce269 100644
--- a/parcels/particle.py
+++ b/parcels/particle.py
@@ -53,7 +53,7 @@ class Variable:
             setattr(instance, f"_{self.name}", value)
 
     def __repr__(self):
-        return f"PVar<{self.name}|{self.dtype}>"
+        return f"Variable(name={self._name}, dtype={self.dtype}, initial={self.initial}, to_write={self.to_write})"
 
     def is64bit(self):
         """Check whether variable is 64-bit."""
diff --git a/parcels/particlefile.py b/parcels/particlefile.py
index c8482b9e..4ab406b4 100644
--- a/parcels/particlefile.py
+++ b/parcels/particlefile.py
@@ -10,7 +10,7 @@ import zarr
 
 import parcels
 from parcels._compat import MPI
-from parcels.tools._helpers import deprecated, deprecated_made_private, timedelta_to_float
+from parcels.tools._helpers import default_repr, deprecated, deprecated_made_private, timedelta_to_float
 from parcels.tools.warnings import FileWarning
 
 __all__ = ["ParticleFile"]
@@ -116,6 +116,16 @@ class ParticleFile:
                 fname = name if extension in [".zarr"] else f"{name}.zarr"
         self._fname = fname
 
+    def __repr__(self) -> str:
+        return (
+            f"{type(self).__name__}("
+            f"name={self.fname!r}, "
+            f"particleset={default_repr(self.particleset)}, "
+            f"outputdt={self.outputdt!r}, "
+            f"chunks={self.chunks!r}, "
+            f"create_new_zarrfile={self.create_new_zarrfile!r})"
+        )
+
     @property
     def create_new_zarrfile(self):
         return self._create_new_zarrfile
diff --git a/parcels/particleset.py b/parcels/particleset.py
index 1fde32ca..ffe7eb65 100644
--- a/parcels/particleset.py
+++ b/parcels/particleset.py
@@ -27,7 +27,7 @@ from parcels.kernel import Kernel
 from parcels.particle import JITParticle, Variable
 from parcels.particledata import ParticleData, ParticleDataIterator
 from parcels.particlefile import ParticleFile
-from parcels.tools._helpers import deprecated, deprecated_made_private, timedelta_to_float
+from parcels.tools._helpers import deprecated, deprecated_made_private, particleset_repr, timedelta_to_float
 from parcels.tools.converters import _get_cftime_calendars, convert_to_flat_array
 from parcels.tools.global_statics import get_package_dir
 from parcels.tools.loggers import logger
@@ -112,6 +112,7 @@ class ParticleSet:
         self.fieldset = fieldset
         self.fieldset._check_complete()
         self.time_origin = fieldset.time_origin
+        self._pclass = pclass
 
         # ==== first: create a new subclass of the pclass that includes the required variables ==== #
         # ==== see dynamic-instantiation trick here: https://www.python-course.eu/python3_classes_and_type.php ==== #
@@ -386,8 +387,12 @@ class ParticleSet:
         # ==== to change at some point - len and size are different things ==== #
         return len(self.particledata)
 
+    @property
+    def pclass(self):
+        return self._pclass
+
     def __repr__(self):
-        return "\n".join([str(p) for p in self])
+        return particleset_repr(self)
 
     def __len__(self):
         return len(self.particledata)
diff --git a/parcels/tools/_helpers.py b/parcels/tools/_helpers.py
index 24c8ba9c..9078690c 100644
--- a/parcels/tools/_helpers.py
+++ b/parcels/tools/_helpers.py
@@ -1,12 +1,19 @@
 """Internal helpers for Parcels."""
 
+from __future__ import annotations
+
 import functools
+import textwrap
 import warnings
 from collections.abc import Callable
 from datetime import timedelta
+from typing import TYPE_CHECKING, Any
 
 import numpy as np
 
+if TYPE_CHECKING:
+    from parcels import Field, FieldSet, ParticleSet
+
 PACKAGE = "Parcels"
 
 
@@ -61,6 +68,77 @@ def patch_docstring(obj: Callable, extra: str) -> None:
     obj.__doc__ = f"{obj.__doc__ or ''}{extra}".strip()
 
 
+def field_repr(field: Field) -> str:
+    """Return a pretty repr for Field"""
+    out = f"""<{type(field).__name__}>
+    name            : {field.name!r}
+    grid            : {field.grid!r}
+    extrapolate time: {field.allow_time_extrapolation!r}
+    time_periodic   : {field.time_periodic!r}
+    gridindexingtype: {field.gridindexingtype!r}
+    to_write        : {field.to_write!r}
+"""
+    return textwrap.dedent(out).strip()
+
+
+def _format_list_items_multiline(items: list[str], level: int = 1) -> str:
+    """Given a list of strings, formats them across multiple lines.
+
+    Uses indentation levels of 4 spaces provided by ``level``.
+
+    Example
+    -------
+    >>> output = _format_list_items_multiline(["item1", "item2", "item3"], 4)
+    >>> f"my_items: {output}"
+    my_items: [
+        item1,
+        item2,
+        item3,
+    ]
+    """
+    if len(items) == 0:
+        return "[]"
+
+    assert level >= 1, "Indentation level >=1 supported"
+    indentation_str = level * 4 * " "
+    indentation_str_end = (level - 1) * 4 * " "
+
+    items_str = ",\n".join([textwrap.indent(i, indentation_str) for i in items])
+    return f"[\n{items_str}\n{indentation_str_end}]"
+
+
+def particleset_repr(pset: ParticleSet) -> str:
+    """Return a pretty repr for ParticleSet"""
+    if len(pset) < 10:
+        particles = [repr(p) for p in pset]
+    else:
+        particles = [repr(pset[i]) for i in range(7)] + ["..."]
+
+    out = f"""<{type(pset).__name__}>
+    fieldset   : {pset.fieldset}
+    pclass     : {pset.pclass}
+    repeatdt   : {pset.repeatdt}
+    # particles: {len(pset)}
+    particles  : {_format_list_items_multiline(particles, level=2)}
+"""
+    return textwrap.dedent(out).strip()
+
+
+def fieldset_repr(fieldset: FieldSet) -> str:
+    """Return a pretty repr for FieldSet"""
+    fields_repr = "\n".join([repr(f) for f in fieldset.get_fields()])
+
+    out = f"""<{type(fieldset).__name__}>
+    fields:
+{textwrap.indent(fields_repr, 8 * " ")}
+"""
+    return textwrap.dedent(out).strip()
+
+
+def default_repr(obj: Any):
+    return object.__repr__(obj)
+
+
 def timedelta_to_float(dt: float | timedelta | np.timedelta64) -> float:
     """Convert a timedelta to a float in seconds."""
     if isinstance(dt, timedelta):


Test patch:
diff --git a/tests/test_reprs.py b/tests/test_reprs.py
index d072ded9..bce4bce4 100644
--- a/tests/test_reprs.py
+++ b/tests/test_reprs.py
@@ -1,12 +1,16 @@
+import re
+from datetime import timedelta
 from typing import Any
 
 import numpy as np
 
-from parcels import Grid, TimeConverter
+import parcels
+from parcels import Grid, ParticleFile, TimeConverter, Variable
 from parcels.grid import RectilinearGrid
+from tests.utils import create_fieldset_unit_mesh, create_simple_pset
 
 
-def validate_simple_repr(class_: type, kwargs: dict[str, Any]):
+def assert_simple_repr(class_: type, kwargs: dict[str, Any]):
     """Test that the repr of an object contains all the arguments. This only works for objects where the repr matches the calling signature."""
     obj = class_(**kwargs)
     obj_repr = repr(obj)
@@ -17,12 +21,48 @@ def validate_simple_repr(class_: type, kwargs: dict[str, Any]):
     assert class_.__name__ in obj_repr
 
 
+def valid_indentation(s: str) -> bool:
+    """Make sure that all lines in string is indented with a multiple of 4 spaces."""
+    if s.startswith(" "):
+        return False
+
+    lines = s.split("\n")
+    for line in lines:
+        line = re.sub("^( {4})+", "", line)
+        if line.startswith(" "):
+            return False
+    return True
+
+
+def test_check_indentation():
+    valid = """
+test
+    test
+test
+    test
+        test
+    test"""
+    assert valid_indentation(valid)
+    invalid = """
+test
+    test
+ invalid!
+"""
+    assert not valid_indentation(invalid)
+
+
 def test_grid_repr():
     """Test arguments are in the repr of a Grid object"""
     kwargs = dict(
         lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, time_origin=TimeConverter(), mesh="spherical"
     )
-    validate_simple_repr(Grid, kwargs)
+    assert_simple_repr(Grid, kwargs)
+
+
+def test_variable_repr():
+    """Test arguments are in the repr of the Variable object."""
+    kwargs = dict(name="test", dtype=np.float32, initial=0, to_write=False)
+    assert_simple_repr(Variable, kwargs)
 
 
 def test_rectilineargrid_repr():
@@ -34,4 +74,41 @@ def test_rectilineargrid_repr():
     kwargs = dict(
         lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, time_origin=TimeConverter(), mesh="spherical"
     )
-    validate_simple_repr(RectilinearGrid, kwargs)
+    assert_simple_repr(RectilinearGrid, kwargs)
+
+
+def test_particlefile_repr():
+    pset = create_simple_pset()
+    kwargs = dict(
+        name="file.zarr", particleset=pset, outputdt=timedelta(hours=1), chunks=None, create_new_zarrfile=False
+    )
+    assert_simple_repr(ParticleFile, kwargs)
+
+
+def test_field_repr():
+    field = create_fieldset_unit_mesh().U
+    assert valid_indentation(repr(field))
+
+
+def test_vectorfield_repr():
+    field = create_fieldset_unit_mesh().UV
+    assert isinstance(field, parcels.VectorField)
+    assert valid_indentation(repr(field))
+
+
+def test_fieldset_repr():
+    fieldset = create_fieldset_unit_mesh()
+    assert valid_indentation(repr(fieldset))
+
+
+def test_particleset_repr():
+    pset = create_simple_pset()
+    valid_indentation(repr(pset))
+
+    pset = create_simple_pset(n=15)
+    valid_indentation(repr(pset))
+
+
+def capture(s):
+    with open("file.txt", "a") as f:
+        f.write(s)
diff --git a/tests/tools/test_helpers.py b/tests/tools/test_helpers.py
index 3e6f5a99..1403b679 100644
--- a/tests/tools/test_helpers.py
+++ b/tests/tools/test_helpers.py
@@ -3,9 +3,20 @@ from datetime import timedelta
 import numpy as np
 import pytest
 
+import parcels.tools._helpers as helpers
 from parcels.tools._helpers import deprecated, deprecated_made_private, timedelta_to_float
 
 
+def test_format_list_items_multiline():
+    expected = """[
+    item1,
+    item2,
+    item3
+]"""
+    assert helpers._format_list_items_multiline(["item1", "item2", "item3"], 1) == expected
+    assert helpers._format_list_items_multiline([], 1) == "[]"
+
+
 def test_deprecated():
     class SomeClass:
         @deprecated()
diff --git a/tests/utils.py b/tests/utils.py
index a5153f6b..14ef8b7e 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,6 +4,7 @@ from pathlib import Path
 
 import numpy as np
 
+import parcels
 from parcels import FieldSet
 
 PROJECT_ROOT = Path(__file__).resolve().parents[1]
@@ -47,6 +48,18 @@ def create_fieldset_zeros_conversion(mesh="spherical", xdim=200, ydim=100, mesh_
     return FieldSet.from_data(data, dimensions, mesh=mesh)
 
 
+def create_simple_pset(n=1):
+    zeros = np.zeros(n)
+    return parcels.ParticleSet(
+        fieldset=create_fieldset_unit_mesh(),
+        pclass=parcels.ScipyParticle,
+        lon=zeros,
+        lat=zeros,
+        depth=zeros,
+        time=zeros,
+    )
+
+
 def create_spherical_positions(n_particles, max_depth=100000):
     yrange = 2 * np.random.rand(n_particles)
     lat = 180 * (np.arccos(1 - yrange) - 0.5 * np.pi) / np.pi