diff --git a/pykusto/expressions.py b/pykusto/expressions.py
index b1eacc0..e50d754 100644
--- a/pykusto/expressions.py
+++ b/pykusto/expressions.py
@@ -107,12 +107,18 @@ class BaseExpression:
     def __ne__(self, other: ExpressionType) -> 'BooleanExpression':
         return BooleanExpression.binary_op(self, ' != ', other)
 
-    def is_in(self, other: ArrayType) -> 'BooleanExpression':
+    def is_in(self, other: DynamicType) -> 'BooleanExpression':
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/dynamic#operators-and-functions-over-dynamic-types
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/inoperator
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datatypes-string-operators
         """
-        return BooleanExpression.binary_op(self, ' in ', other)
+        if isinstance(other, (List, Tuple)):
+            # For a literal array, we can use 'in'
+            # The following RHS is the only place where a literal list does not require being surrounded by 'dynamic()'
+            return BooleanExpression(KQL(f'{self.kql} in ({", ".join(map(to_kql, other))})'))
+        # Otherwise, for some reason Kusto does not accept 'in', and we need to use 'contains' as if 'other' was a string
+        return BooleanExpression.binary_op(other, ' contains ', self)
 
     def is_null(self) -> 'BooleanExpression':
         """
@@ -678,10 +684,33 @@ class TimespanExpression(BaseExpression):
         return BooleanExpression(KQL(f'{self.kql} between ({_subexpr_to_kql(lower)} .. {_subexpr_to_kql(upper)})'))
 
 
+class BaseDynamicExpression(BaseExpression):
+    # We would prefer to use 'abc' to make the class abstract, but this can be done only if there is at least one
+    # abstract method, which we don't have here. Overriding __new___ is the next best solution.
+    def __new__(cls, *args, **kwargs) -> 'BaseDynamicExpression':
+        assert cls is not BaseDynamicExpression, "BaseDynamicExpression is abstract"
+        return object.__new__(cls)
+
+    def __getitem__(self, index: Union[StringType, NumberType]) -> 'AnyExpression':
+        return AnyExpression(KQL(f'{self.kql}[{to_kql(index)}]'))
+
+    def contains(self, other: ExpressionType) -> 'BooleanExpression':
+        """
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/dynamic#operators-and-functions-over-dynamic-types
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/inoperator
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datatypes-string-operators
+        """
+        # For some reason Kusto does not accept 'in', and we need to use 'contains' as if this were a string
+        if not isinstance(other, (BaseExpression, str)):
+            # When 'other' is a literal, it has to be a string, because syntactically 'contains' works only on strings.
+            other = str(to_kql(other))
+        return BooleanExpression.binary_op(self, ' contains ', other)
+
+
 @plain_expression(KustoType.ARRAY)
-class ArrayExpression(BaseExpression):
+class ArrayExpression(BaseDynamicExpression):
     def __getitem__(self, index: NumberType) -> 'AnyExpression':
-        return AnyExpression(KQL(f'{self.kql}[{to_kql(index)}]'))
+        return super().__getitem__(index)
 
     # We would like to allow using len(), but Python requires it to return an int, so we can't
     def array_length(self) -> NumberExpression:
@@ -694,17 +723,18 @@ class ArrayExpression(BaseExpression):
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/dynamic#operators-and-functions-over-dynamic-types
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/inoperator
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datatypes-string-operators
         """
-        return BooleanExpression.binary_op(other, ' in ', self)
+        return self.contains(other)
 
     def assign_to_multiple_columns(self, *columns: 'AnyTypeColumn') -> 'AssignmentToMultipleColumns':
         return AssignmentToMultipleColumns(columns, self)
 
 
 @plain_expression(KustoType.MAPPING)
-class MappingExpression(BaseExpression):
+class MappingExpression(BaseDynamicExpression):
     def __getitem__(self, index: StringType) -> 'AnyExpression':
-        return AnyExpression(KQL(f'{self.kql}[{to_kql(index)}]'))
+        return super().__getitem__(index)
 
     def __getattr__(self, name: str) -> 'AnyExpression':
         return AnyExpression(KQL(f'{self.kql}.{name}'))
@@ -715,10 +745,17 @@ class MappingExpression(BaseExpression):
         """
         return ArrayExpression(KQL(f'bag_keys({self.kql})'))
 
+    def bag_contains(self, other: ExpressionType) -> 'BooleanExpression':
+        """
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/dynamic#operators-and-functions-over-dynamic-types
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/inoperator
+        https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/datatypes-string-operators
+        """
+        return self.contains(other)
+
 
 class DynamicExpression(ArrayExpression, MappingExpression):
-    def __getitem__(self, index: Union[StringType, NumberType]) -> 'AnyExpression':
-        return AnyExpression(KQL(f'{self.kql}[{_subexpr_to_kql(index)}]'))
+    pass
 
 
 class AnyExpression(
diff --git a/pykusto/functions.py b/pykusto/functions.py
index 6e09521..7f65240 100644
--- a/pykusto/functions.py
+++ b/pykusto/functions.py
@@ -1,4 +1,3 @@
-import json
 from itertools import chain
 from typing import Union
 
@@ -18,6 +17,7 @@ class Functions:
     Recommended import style:\n
     `from pykusto.functions import Functions as f`
     """
+
     # Scalar functions
 
     @staticmethod
@@ -894,18 +894,12 @@ class Functions:
             raise ValueError("strcat requires at least two arguments")
         return StringExpression(KQL(f"strcat({', '.join(to_kql(s) for s in strings)})"))
 
-    @staticmethod
-    def to_literal_dynamic(d: DynamicType) -> KQL:
-        if isinstance(d, BaseExpression):
-            return d.kql
-        return KQL(f'dynamic({json.dumps(d)})')
-
     @staticmethod
     def strcat_array(expr: ArrayType, delimiter: StringType) -> StringExpression:
         """
         https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/strcat-arrayfunction
         """
-        return StringExpression(KQL(f'strcat_array({Functions.to_literal_dynamic(expr)}, {to_kql(delimiter)})'))
+        return StringExpression(KQL(f'strcat_array({to_kql(expr)}, {to_kql(delimiter)})'))
 
     @staticmethod
     def strcat_delim(delimiter: StringType, expr1: StringType, expr2: StringType, *expressions: StringType) -> StringExpression:
diff --git a/pykusto/kql_converters.py b/pykusto/kql_converters.py
index e177728..7058eff 100644
--- a/pykusto/kql_converters.py
+++ b/pykusto/kql_converters.py
@@ -24,7 +24,7 @@ def timedelta_to_kql(td: timedelta) -> KQL:
 @kql_converter(KustoType.ARRAY, KustoType.MAPPING)
 def dynamic_to_kql(d: Union[Mapping, List, Tuple]) -> KQL:
     try:
-        query = list(json.dumps(d))
+        return KQL(f"dynamic({json.dumps(d)})")
     except TypeError:
         # Using exceptions as part of normal flow is not best practice, however in this case we have a good reason.
         # The given object might contain a non-primitive object somewhere inside it, and the only way to find it is to go through the entire hierarchy, which is exactly
@@ -32,29 +32,8 @@ def dynamic_to_kql(d: Union[Mapping, List, Tuple]) -> KQL:
         # Also keep in mind that exception handling in Python has no performance overhead (unlike e.g. Java).
         return build_dynamic(d)
 
-    # Convert square brackets to round brackets (Issue #11)
-    counter = 0
-    prev = ""
-    for i, c in enumerate(query):
-        if counter == 0:
-            if c == "[":
-                query[i] = "("
-            elif c == "]":
-                query[i] = ")"
-            elif c in ['"', '\''] and prev != "\\":
-                counter += 1
-        elif counter > 0:
-            if c in ['"', '\''] and prev != "\\":
-                counter -= 1
-        prev = query[i]
-    assert counter == 0
-    return KQL("".join(query))
-
 
 def build_dynamic(d: Union[Mapping, List, Tuple]) -> KQL:
-    from pykusto.expressions import BaseExpression
-    if isinstance(d, BaseExpression):
-        return d.kql
     if isinstance(d, Mapping):
         return KQL(f"pack({', '.join(map(build_dynamic, chain(*d.items())))})")
     if isinstance(d, (List, Tuple)):

Converting a Python literal list to KQL fails
Should probably use dynamic()
diff --git a/test/test_expressions.py b/test/test_expressions.py
index 0e47b49..9bf4a43 100644
--- a/test/test_expressions.py
+++ b/test/test_expressions.py
@@ -35,10 +35,16 @@ class TestExpressions(TestBase):
 
     def test_array_contains(self):
         self.assertEqual(
-            ' | where true in arrayField',
+            ' | where arrayField contains "true"',
             Query().where(t.arrayField.array_contains(True)).render(),
         )
 
+    def test_bag_contains(self):
+        self.assertEqual(
+            ' | where mapField contains "2"',
+            Query().where(t.mapField.bag_contains(2)).render(),
+        )
+
     def test_not_equals(self):
         self.assertEqual(
             ' | where stringField != "bar"',
@@ -333,7 +339,7 @@ class TestExpressions(TestBase):
 
     def test_dynamic(self):
         self.assertEqual(
-            ' | where (mapField["foo"][0].bar[1][2][(tolower(stringField))]) > time(1.0:0:0.0)',
+            ' | where (mapField["foo"][0].bar[1][2][tolower(stringField)]) > time(1.0:0:0.0)',
             Query().where(t.mapField['foo'][0].bar[1][2][t.stringField.lower()] > timedelta(1)).render(),
         )
 
@@ -391,6 +397,12 @@ class TestExpressions(TestBase):
             lambda: t.stringField in t.stringField2
         )
 
+    def test_is_in_expression(self):
+        self.assertEqual(
+            ' | where arrayField contains stringField',
+            Query().where(t.stringField.is_in(t.arrayField)).render()
+        )
+
     def test_has(self):
         self.assertEqual(
             ' | where stringField has "test"',
diff --git a/test/test_utils.py b/test/test_utils.py
index a38c620..5f3b4c5 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -13,8 +13,8 @@ class TestUtils(TestBase):
             "pets": ["Libby", "Panda", "]", "["]
         }
         self.assertEqual(
-            '{"name": "Alan", "age": 21, "address": ("NY", 36), '
-            '"pets": ("Libby", "Panda", "]", "[")}',
+            'dynamic({"name": "Alan", "age": 21, "address": ["NY", 36], '
+            '"pets": ["Libby", "Panda", "]", "["]})',
             to_kql(test_dict)
         )
 
@@ -25,6 +25,7 @@ class TestUtils(TestBase):
         def str_annotated(s: str) -> str:
             return "response to " + s
 
+        # noinspection PyTypeChecker
         self.assertEqual(
             "response to test for_type",
             test_annotation.for_type(str)("test for_type")
@@ -49,6 +50,7 @@ class TestUtils(TestBase):
         def str_annotated(s: str) -> str:
             return "response to " + s
 
+        # noinspection PyTypeChecker
         self.assertRaises(
             ValueError("Test annotation: no registered callable for type bool"),
             lambda: test_annotation.for_type(bool)("test for_type")

pip install -e .[dev]
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
adal==1.2.7
attrs @ file:///croot/attrs_1668696182826/work
azure-core==1.30.1
azure-kusto-data==0.1.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
cryptography==44.0.2
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isodate==0.7.2
msrest==0.7.1
msrestazure==0.6.4.post1
numpy==1.21.6
oauthlib==3.2.2
packaging @ file:///croot/packaging_1671697413597/work
pandas==1.0.4
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
PyJWT==2.8.0
-e git+https://github.com/Azure/pykusto.git@89235ad79caf383a1b5e719e8f83552025750896#egg=pykusto
pytest==7.1.2
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.31.0
requests-oauthlib==2.0.0
six==1.17.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions==4.7.1
urllib3==2.0.7
zipp @ file:///croot/zipp_1672387121353/work

name: pykusto
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - adal==1.2.7
      - azure-core==1.30.1
      - azure-kusto-data==0.1.0
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - cryptography==44.0.2
      - idna==3.10
      - isodate==0.7.2
      - msrest==0.7.1
      - msrestazure==0.6.4.post1
      - numpy==1.21.6
      - oauthlib==3.2.2
      - pandas==1.0.4
      - pycparser==2.21
      - pyjwt==2.8.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.31.0
      - requests-oauthlib==2.0.0
      - six==1.17.0
      - typing-extensions==4.7.1
      - urllib3==2.0.7
prefix: /opt/conda/envs/pykusto

test/test_expressions.py::TestExpressions::test_array_contains test/test_expressions.py::TestExpressions::test_bag_contains test/test_expressions.py::TestExpressions::test_dynamic test/test_expressions.py::TestExpressions::test_is_in_expression test/test_utils.py::TestUtils::test_dynamic_to_kql
test/test_expressions.py::TestExpressions::test_abs test/test_expressions.py::TestExpressions::test_add_number_column test/test_expressions.py::TestExpressions::test_add_swapped_timespan_to_timespan test/test_expressions.py::TestExpressions::test_add_timespan_to_date test/test_expressions.py::TestExpressions::test_add_timespan_to_timespan test/test_expressions.py::TestExpressions::test_and test/test_expressions.py::TestExpressions::test_array_access test/test_expressions.py::TestExpressions::test_array_access_expression_index test/test_expressions.py::TestExpressions::test_array_access_yields_any_expression test/test_expressions.py::TestExpressions::test_assign_to test/test_expressions.py::TestExpressions::test_between test/test_expressions.py::TestExpressions::test_between_date test/test_expressions.py::TestExpressions::test_between_timespan test/test_expressions.py::TestExpressions::test_bin_auto test/test_expressions.py::TestExpressions::test_column_generator test/test_expressions.py::TestExpressions::test_column_name_quoting test/test_expressions.py::TestExpressions::test_column_with_digits test/test_expressions.py::TestExpressions::test_column_with_dot test/test_expressions.py::TestExpressions::test_contains test/test_expressions.py::TestExpressions::test_div test/test_expressions.py::TestExpressions::test_extend_const test/test_expressions.py::TestExpressions::test_ge test/test_expressions.py::TestExpressions::test_ge_date test/test_expressions.py::TestExpressions::test_gt_date test/test_expressions.py::TestExpressions::test_has test/test_expressions.py::TestExpressions::test_is_empty test/test_expressions.py::TestExpressions::test_is_in test/test_expressions.py::TestExpressions::test_le_date test/test_expressions.py::TestExpressions::test_lt_date test/test_expressions.py::TestExpressions::test_mapping_access test/test_expressions.py::TestExpressions::test_mapping_access_attribute test/test_expressions.py::TestExpressions::test_mapping_access_expression_index test/test_expressions.py::TestExpressions::test_mapping_access_yields_any_expression test/test_expressions.py::TestExpressions::test_mod test/test_expressions.py::TestExpressions::test_multiply_number_column test/test_expressions.py::TestExpressions::test_multiply_number_expression test/test_expressions.py::TestExpressions::test_negation test/test_expressions.py::TestExpressions::test_not test/test_expressions.py::TestExpressions::test_not_contains test/test_expressions.py::TestExpressions::test_not_equals test/test_expressions.py::TestExpressions::test_or test/test_expressions.py::TestExpressions::test_repr test/test_expressions.py::TestExpressions::test_str_ends_with test/test_expressions.py::TestExpressions::test_str_equals test/test_expressions.py::TestExpressions::test_str_matches test/test_expressions.py::TestExpressions::test_str_not_equals test/test_expressions.py::TestExpressions::test_str_starts_with test/test_expressions.py::TestExpressions::test_sub_date_unknown_column test/test_expressions.py::TestExpressions::test_sub_date_unknown_type test/test_expressions.py::TestExpressions::test_sub_datetime test/test_expressions.py::TestExpressions::test_sub_from_datetime test/test_expressions.py::TestExpressions::test_sub_from_number test/test_expressions.py::TestExpressions::test_sub_timespan test/test_expressions.py::TestExpressions::test_sub_unknown_type_datetime test/test_expressions.py::TestExpressions::test_sub_unknown_type_number test/test_expressions.py::TestExpressions::test_sub_unknown_type_timespan test/test_expressions.py::TestExpressions::test_subtract_timespan_from_timespan test/test_expressions.py::TestExpressions::test_swapped_and test/test_expressions.py::TestExpressions::test_swapped_div test/test_expressions.py::TestExpressions::test_swapped_mod test/test_expressions.py::TestExpressions::test_swapped_or test/test_expressions.py::TestExpressions::test_swapped_subtract_timespan_from_timespan test/test_expressions.py::TestExpressions::test_to_bool test/test_expressions.py::TestExpressions::test_to_int test/test_expressions.py::TestExpressions::test_to_long test/test_utils.py::TestUtils::test_type_registrar_collision test/test_utils.py::TestUtils::test_type_registrar_for_obj test/test_utils.py::TestUtils::test_type_registrar_for_obj_not_found test/test_utils.py::TestUtils::test_type_registrar_for_type test/test_utils.py::TestUtils::test_type_registrar_for_type_not_found
pykusto/functions.py pykusto/expressions.py pykusto/kql_converters.py
pykusto/functions.py pykusto/expressions.py pykusto/kql_converters.py